diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 74196cdc625..8435fe73d40 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -256,22 +256,39 @@ async def async_setup_hass( runtime_config: RuntimeConfig, ) -> core.HomeAssistant | None: """Set up Home Assistant.""" - hass = core.HomeAssistant(runtime_config.config_dir) - async_enable_logging( - hass, - runtime_config.verbose, - runtime_config.log_rotate_days, - runtime_config.log_file, - runtime_config.log_no_color, - ) + def create_hass() -> core.HomeAssistant: + """Create the hass object and do basic setup.""" + hass = core.HomeAssistant(runtime_config.config_dir) + loader.async_setup(hass) - if runtime_config.debug or hass.loop.get_debug(): - hass.config.debug = True + async_enable_logging( + hass, + runtime_config.verbose, + runtime_config.log_rotate_days, + runtime_config.log_file, + runtime_config.log_no_color, + ) + + if runtime_config.debug or hass.loop.get_debug(): + hass.config.debug = True + + hass.config.safe_mode = runtime_config.safe_mode + hass.config.skip_pip = runtime_config.skip_pip + hass.config.skip_pip_packages = runtime_config.skip_pip_packages + + return hass + + async def stop_hass(hass: core.HomeAssistant) -> None: + """Stop hass.""" + # Ask integrations to shut down. It's messy but we can't + # do a clean stop without knowing what is broken + with contextlib.suppress(TimeoutError): + async with hass.timeout.async_timeout(10): + await hass.async_stop() + + hass = create_hass() - hass.config.safe_mode = runtime_config.safe_mode - hass.config.skip_pip = runtime_config.skip_pip - hass.config.skip_pip_packages = runtime_config.skip_pip_packages if runtime_config.skip_pip or runtime_config.skip_pip_packages: _LOGGER.warning( "Skipping pip installation of required modules. This may cause issues" @@ -283,7 +300,6 @@ async def async_setup_hass( _LOGGER.info("Config directory: %s", runtime_config.config_dir) - loader.async_setup(hass) block_async_io.enable() config_dict = None @@ -309,27 +325,28 @@ async def async_setup_hass( if config_dict is None: recovery_mode = True + await stop_hass(hass) + hass = 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() elif any(domain not in hass.config.components for domain in CRITICAL_INTEGRATIONS): _LOGGER.warning( "Detected that %s did not load. Activating recovery mode", ",".join(CRITICAL_INTEGRATIONS), ) - # Ask integrations to shut down. It's messy but we can't - # do a clean stop without knowing what is broken - with contextlib.suppress(TimeoutError): - async with hass.timeout.async_timeout(10): - await hass.async_stop() - recovery_mode = True old_config = hass.config old_logging = hass.data.get(DATA_LOGGING) - hass = core.HomeAssistant(old_config.config_dir) + recovery_mode = True + await stop_hass(hass) + hass = create_hass() + if old_logging: hass.data[DATA_LOGGING] = old_logging hass.config.debug = old_config.debug diff --git a/homeassistant/components/aemet/manifest.json b/homeassistant/components/aemet/manifest.json index b8a19bcd27a..8a22385f82b 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.1"] + "requirements": ["AEMET-OpenData==0.5.2"] } diff --git a/homeassistant/components/canary/manifest.json b/homeassistant/components/canary/manifest.json index e6bc52540d5..4d5adf4a32b 100644 --- a/homeassistant/components/canary/manifest.json +++ b/homeassistant/components/canary/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/canary", "iot_class": "cloud_polling", "loggers": ["canary"], - "requirements": ["py-canary==0.5.3"] + "requirements": ["py-canary==0.5.4"] } diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index a3af6607aba..ee0b29f22fc 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.5"] + "requirements": ["hassil==1.7.1", "home-assistant-intents==2024.6.21"] } diff --git a/homeassistant/components/ecobee/weather.py b/homeassistant/components/ecobee/weather.py index b7961f956eb..b6378504c65 100644 --- a/homeassistant/components/ecobee/weather.py +++ b/homeassistant/components/ecobee/weather.py @@ -59,7 +59,7 @@ class EcobeeWeather(WeatherEntity): _attr_native_pressure_unit = UnitOfPressure.HPA _attr_native_temperature_unit = UnitOfTemperature.FAHRENHEIT _attr_native_visibility_unit = UnitOfLength.METERS - _attr_native_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND + _attr_native_wind_speed_unit = UnitOfSpeed.MILES_PER_HOUR _attr_has_entity_name = True _attr_name = None _attr_supported_features = WeatherEntityFeature.FORECAST_DAILY diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index f29c8177dfd..a0bdd5d4919 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.2"] + "requirements": ["env-canada==0.6.3"] } diff --git a/homeassistant/components/holiday/manifest.json b/homeassistant/components/holiday/manifest.json index bc7ce0e8dd1..cb67039f374 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.50", "babel==2.13.1"] + "requirements": ["holidays==0.51", "babel==2.15.0"] } diff --git a/homeassistant/components/hydrawise/manifest.json b/homeassistant/components/hydrawise/manifest.json index dc6408407e7..b85ddca042e 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.3"] + "requirements": ["pydrawise==2024.6.4"] } diff --git a/homeassistant/components/hydrawise/sensor.py b/homeassistant/components/hydrawise/sensor.py index 87dc5e73afe..fe4b33d5851 100644 --- a/homeassistant/components/hydrawise/sensor.py +++ b/homeassistant/components/hydrawise/sensor.py @@ -48,7 +48,7 @@ def _get_zone_daily_active_water_use(sensor: HydrawiseSensor) -> float: return float(daily_water_summary.active_use_by_zone_id.get(sensor.zone.id, 0.0)) -def _get_controller_daily_active_water_use(sensor: HydrawiseSensor) -> float: +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] return daily_water_summary.total_active_use @@ -71,7 +71,6 @@ FLOW_CONTROLLER_SENSORS: tuple[HydrawiseSensorEntityDescription, ...] = ( key="daily_total_water_use", translation_key="daily_total_water_use", device_class=SensorDeviceClass.VOLUME, - native_unit_of_measurement=UnitOfVolume.GALLONS, suggested_display_precision=1, value_fn=_get_controller_daily_total_water_use, ), @@ -79,7 +78,6 @@ FLOW_CONTROLLER_SENSORS: tuple[HydrawiseSensorEntityDescription, ...] = ( key="daily_active_water_use", translation_key="daily_active_water_use", device_class=SensorDeviceClass.VOLUME, - native_unit_of_measurement=UnitOfVolume.GALLONS, suggested_display_precision=1, value_fn=_get_controller_daily_active_water_use, ), @@ -87,7 +85,6 @@ FLOW_CONTROLLER_SENSORS: tuple[HydrawiseSensorEntityDescription, ...] = ( key="daily_inactive_water_use", translation_key="daily_inactive_water_use", device_class=SensorDeviceClass.VOLUME, - native_unit_of_measurement=UnitOfVolume.GALLONS, suggested_display_precision=1, value_fn=_get_controller_daily_inactive_water_use, ), @@ -98,7 +95,6 @@ FLOW_ZONE_SENSORS: tuple[SensorEntityDescription, ...] = ( key="daily_active_water_use", translation_key="daily_active_water_use", device_class=SensorDeviceClass.VOLUME, - native_unit_of_measurement=UnitOfVolume.GALLONS, suggested_display_precision=1, value_fn=_get_zone_daily_active_water_use, ), @@ -165,6 +161,17 @@ class HydrawiseSensor(HydrawiseEntity, SensorEntity): entity_description: HydrawiseSensorEntityDescription + @property + def native_unit_of_measurement(self) -> str | None: + """Return the unit_of_measurement of the sensor.""" + if self.entity_description.device_class != SensorDeviceClass.VOLUME: + return self.entity_description.native_unit_of_measurement + return ( + UnitOfVolume.GALLONS + if self.coordinator.data.user.units.units_name == "imperial" + else UnitOfVolume.LITERS + ) + @property def icon(self) -> str | None: """Icon of the entity based on the value.""" diff --git a/homeassistant/components/imap/manifest.json b/homeassistant/components/imap/manifest.json index 3c35d00f714..b058a3d50f4 100644 --- a/homeassistant/components/imap/manifest.json +++ b/homeassistant/components/imap/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/imap", "iot_class": "cloud_push", "loggers": ["aioimaplib"], - "requirements": ["aioimaplib==1.0.1"] + "requirements": ["aioimaplib==1.1.0"] } diff --git a/homeassistant/components/jewish_calendar/__init__.py b/homeassistant/components/jewish_calendar/__init__.py index 8383f9181fc..81fe6cb5377 100644 --- a/homeassistant/components/jewish_calendar/__init__.py +++ b/homeassistant/components/jewish_calendar/__init__.py @@ -72,11 +72,14 @@ def get_unique_prefix( havdalah_offset: int | None, ) -> str: """Create a prefix for unique ids.""" + # location.altitude was unset before 2024.6 when this method + # was used to create the unique id. As such it would always + # use the default altitude of 754. config_properties = [ location.latitude, location.longitude, location.timezone, - location.altitude, + 754, location.diaspora, language, candle_lighting_offset, diff --git a/homeassistant/components/jewish_calendar/manifest.json b/homeassistant/components/jewish_calendar/manifest.json index 20eb28929bd..6d2fe8ecfa1 100644 --- a/homeassistant/components/jewish_calendar/manifest.json +++ b/homeassistant/components/jewish_calendar/manifest.json @@ -6,6 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/jewish_calendar", "iot_class": "calculated", "loggers": ["hdate"], - "requirements": ["hdate==0.10.8"], + "requirements": ["hdate==0.10.9"], "single_config_entry": true } diff --git a/homeassistant/components/matter/light.py b/homeassistant/components/matter/light.py index 89400c98989..007bcd1a33a 100644 --- a/homeassistant/components/matter/light.py +++ b/homeassistant/components/matter/light.py @@ -56,6 +56,7 @@ TRANSITION_BLOCKLIST = ( (5010, 769, "3.0", "1.0.0"), (4999, 25057, "1.0", "27.0"), (4448, 36866, "V1", "V1.0.0.5"), + (5009, 514, "1.0", "1.0.0"), ) diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py index 7575443c793..97e0b3e3631 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -341,7 +341,7 @@ class OnkyoDevice(MediaPlayerEntity): 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) + # AMP_VOL / (MAX_RECEIVER_VOL * (MAX_VOL / 100)) self._attr_volume_level = volume_raw[1] / ( self._receiver_max_volume * self._max_volume / 100 ) @@ -511,9 +511,9 @@ class OnkyoDeviceZone(OnkyoDevice): 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) + # AMP_VOL / (MAX_RECEIVER_VOL * (MAX_VOL / 100)) + self._attr_volume_level = volume_raw[1] / ( + self._receiver_max_volume * self._max_volume / 100 ) @property diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index ada7d2d2533..b1937ee219d 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.3"], + "requirements": ["plugwise==0.37.4.1"], "zeroconf": ["_plugwise._tcp.local."] } diff --git a/homeassistant/components/songpal/media_player.py b/homeassistant/components/songpal/media_player.py index c6d6524cefb..9f828591a08 100644 --- a/homeassistant/components/songpal/media_player.py +++ b/homeassistant/components/songpal/media_player.py @@ -140,7 +140,12 @@ class SongpalEntity(MediaPlayerEntity): async def _get_sound_modes_info(self): """Get available sound modes and the active one.""" - settings = await self._dev.get_sound_settings("soundField") + for settings in await self._dev.get_sound_settings(): + if settings.target == "soundField": + break + else: + return None, {} + if isinstance(settings, Setting): settings = [settings] diff --git a/homeassistant/components/spotify/__init__.py b/homeassistant/components/spotify/__init__.py index 632871ba36e..becf90b04cd 100644 --- a/homeassistant/components/spotify/__init__.py +++ b/homeassistant/components/spotify/__init__.py @@ -2,7 +2,6 @@ from __future__ import annotations -from dataclasses import dataclass from datetime import timedelta from typing import Any @@ -22,6 +21,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .browse_media import async_browse_media from .const import DOMAIN, LOGGER, SPOTIFY_SCOPES +from .models import HomeAssistantSpotifyData from .util import ( is_spotify_media_type, resolve_spotify_media_type, @@ -39,16 +39,6 @@ __all__ = [ ] -@dataclass -class HomeAssistantSpotifyData: - """Spotify data stored in the Home Assistant data object.""" - - client: Spotify - current_user: dict[str, Any] - devices: DataUpdateCoordinator[list[dict[str, Any]]] - session: OAuth2Session - - type SpotifyConfigEntry = ConfigEntry[HomeAssistantSpotifyData] diff --git a/homeassistant/components/spotify/browse_media.py b/homeassistant/components/spotify/browse_media.py index a1d3d9c804a..cff7cae5ebd 100644 --- a/homeassistant/components/spotify/browse_media.py +++ b/homeassistant/components/spotify/browse_media.py @@ -5,7 +5,7 @@ from __future__ import annotations from enum import StrEnum from functools import partial import logging -from typing import TYPE_CHECKING, Any +from typing import Any from spotipy import Spotify import yarl @@ -20,11 +20,9 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session from .const import DOMAIN, MEDIA_PLAYER_PREFIX, MEDIA_TYPE_SHOW, PLAYABLE_MEDIA_TYPES +from .models import HomeAssistantSpotifyData from .util import fetch_image_url -if TYPE_CHECKING: - from . import HomeAssistantSpotifyData - BROWSE_LIMIT = 48 diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index fe9614374f7..bd1bcdfd43e 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -29,9 +29,10 @@ from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import utcnow -from . import HomeAssistantSpotifyData, SpotifyConfigEntry +from . import SpotifyConfigEntry from .browse_media import async_browse_media_internal from .const import DOMAIN, MEDIA_PLAYER_PREFIX, PLAYABLE_MEDIA_TYPES, SPOTIFY_SCOPES +from .models import HomeAssistantSpotifyData from .util import fetch_image_url _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/spotify/models.py b/homeassistant/components/spotify/models.py new file mode 100644 index 00000000000..bbec134d89d --- /dev/null +++ b/homeassistant/components/spotify/models.py @@ -0,0 +1,19 @@ +"""Models for use in Spotify integration.""" + +from dataclasses import dataclass +from typing import Any + +from spotipy import Spotify + +from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + + +@dataclass +class HomeAssistantSpotifyData: + """Spotify data stored in the Home Assistant data object.""" + + client: Spotify + current_user: dict[str, Any] + devices: DataUpdateCoordinator[list[dict[str, Any]]] + session: OAuth2Session diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index 11839caf8be..e6367458578 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -46,6 +46,8 @@ DEFAULT_SNAPSHOT_QUALITY = SNAPSHOT_PROFILE_BALANCED ENTITY_UNIT_LOAD = "load" +SHARED_SUFFIX = "_shared" + # Signals SIGNAL_CAMERA_SOURCE_CHANGED = "synology_dsm.camera_stream_source_changed" diff --git a/homeassistant/components/synology_dsm/media_source.py b/homeassistant/components/synology_dsm/media_source.py index 4b0c19b2b55..ace5733c222 100644 --- a/homeassistant/components/synology_dsm/media_source.py +++ b/homeassistant/components/synology_dsm/media_source.py @@ -21,7 +21,7 @@ from homeassistant.components.media_source import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from .const import DOMAIN +from .const import DOMAIN, SHARED_SUFFIX from .models import SynologyDSMData @@ -45,6 +45,7 @@ class SynologyPhotosMediaSourceIdentifier: self.album_id = None self.cache_key = None self.file_name = None + self.is_shared = False if parts: self.unique_id = parts[0] @@ -54,6 +55,9 @@ class SynologyPhotosMediaSourceIdentifier: 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): @@ -160,10 +164,13 @@ class SynologyPhotosMediaSource(MediaSource): if isinstance(mime_type, str) and mime_type.startswith("image/"): # Force small small thumbnails album_item.thumbnail_size = "sm" + suffix = "" + if album_item.is_shared: + suffix = SHARED_SUFFIX ret.append( BrowseMediaSource( domain=DOMAIN, - identifier=f"{identifier.unique_id}/{identifier.album_id}/{album_item.thumbnail_cache_key}/{album_item.file_name}", + identifier=f"{identifier.unique_id}/{identifier.album_id}/{album_item.thumbnail_cache_key}/{album_item.file_name}{suffix}", media_class=MediaClass.IMAGE, media_content_type=mime_type, title=album_item.file_name, @@ -186,8 +193,11 @@ class SynologyPhotosMediaSource(MediaSource): mime_type, _ = mimetypes.guess_type(identifier.file_name) if not isinstance(mime_type, str): raise Unresolvable("No file extension") + suffix = "" + if identifier.is_shared: + suffix = SHARED_SUFFIX return PlayMedia( - f"/synology_dsm/{identifier.unique_id}/{identifier.cache_key}/{identifier.file_name}", + f"/synology_dsm/{identifier.unique_id}/{identifier.cache_key}/{identifier.file_name}{suffix}", mime_type, ) @@ -223,13 +233,14 @@ class SynologyDsmMediaView(http.HomeAssistantView): # location: {cache_key}/{filename} cache_key, file_name = 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, "", False) + item = SynoPhotosItem(image_id, "", "", "", cache_key, "", shared) try: image = await diskstation.api.photos.download_item(item) except SynologyDSMException as exc: diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index e703f393d68..af4e0fde137 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -164,10 +164,14 @@ class UnifiFlowHandler(ConfigFlow, domain=UNIFI_DOMAIN): abort_reason = "reauth_successful" if config_entry: - hub = config_entry.runtime_data + try: + hub = config_entry.runtime_data - if hub and hub.available: - return self.async_abort(reason="already_configured") + if hub and hub.available: + return self.async_abort(reason="already_configured") + + except AttributeError: + pass return self.async_update_reload_and_abort( config_entry, data=self.config, reason=abort_reason diff --git a/homeassistant/components/weatherflow_cloud/manifest.json b/homeassistant/components/weatherflow_cloud/manifest.json index 361349dcbe8..93df04d833c 100644 --- a/homeassistant/components/weatherflow_cloud/manifest.json +++ b/homeassistant/components/weatherflow_cloud/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/weatherflow_cloud", "iot_class": "cloud_polling", - "requirements": ["weatherflow4py==0.2.20"] + "requirements": ["weatherflow4py==0.2.21"] } diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index 71c26a30e94..1148f46e2d1 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.50"] + "requirements": ["holidays==0.51"] } diff --git a/homeassistant/const.py b/homeassistant/const.py index cd340cd5079..25c5df8a136 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 = 6 -PATCH_VERSION: Final = "3" +PATCH_VERSION: Final = "4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 94f030c6104..5a64438116f 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.0 aiohttp==3.9.5 aiohttp_cors==0.7.0 -aiozoneinfo==0.1.0 +aiozoneinfo==0.2.0 astral==2.2 async-interrupt==1.1.1 async-upnp-client==0.38.3 @@ -33,7 +33,7 @@ hass-nabucasa==0.81.1 hassil==1.7.1 home-assistant-bluetooth==1.12.0 home-assistant-frontend==20240610.1 -home-assistant-intents==2024.6.5 +home-assistant-intents==2024.6.21 httpx==0.27.0 ifaddr==0.2.0 Jinja2==3.1.4 @@ -197,3 +197,6 @@ scapy>=2.5.0 # Only tuf>=4 includes a constraint to <1.0. # https://github.com/theupdateframework/python-tuf/releases/tag/v4.0.0 tuf>=4.0.0 + +# https://github.com/jd/tenacity/issues/471 +tenacity<8.4.0 diff --git a/pyproject.toml b/pyproject.toml index 1ca2b5cb40e..4c11317242e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.6.3" +version = "2024.6.4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" @@ -28,7 +28,7 @@ dependencies = [ "aiohttp_cors==0.7.0", "aiohttp-fast-url-dispatcher==0.3.0", "aiohttp-fast-zlib==0.1.0", - "aiozoneinfo==0.1.0", + "aiozoneinfo==0.2.0", "astral==2.2", "async-interrupt==1.1.1", "attrs==23.2.0", diff --git a/requirements.txt b/requirements.txt index 05b0eb35c1e..4790de4d064 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.0 -aiozoneinfo==0.1.0 +aiozoneinfo==0.2.0 astral==2.2 async-interrupt==1.1.1 attrs==23.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 289a4eead5d..1ccf5efbdc9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -4,7 +4,7 @@ -r requirements.txt # homeassistant.components.aemet -AEMET-OpenData==0.5.1 +AEMET-OpenData==0.5.2 # homeassistant.components.honeywell AIOSomecomfort==0.0.25 @@ -261,7 +261,7 @@ aiohomekit==3.1.5 aiohue==4.7.1 # homeassistant.components.imap -aioimaplib==1.0.1 +aioimaplib==1.1.0 # homeassistant.components.apache_kafka aiokafka==0.10.0 @@ -526,7 +526,7 @@ azure-kusto-ingest==3.1.0 azure-servicebus==7.10.0 # homeassistant.components.holiday -babel==2.13.1 +babel==2.15.0 # homeassistant.components.baidu baidu-aip==1.6.6 @@ -810,7 +810,7 @@ enocean==0.50 enturclient==0.2.4 # homeassistant.components.environment_canada -env-canada==0.6.2 +env-canada==0.6.3 # homeassistant.components.season ephem==4.1.5 @@ -1056,7 +1056,7 @@ hass-splunk==0.1.1 hassil==1.7.1 # homeassistant.components.jewish_calendar -hdate==0.10.8 +hdate==0.10.9 # homeassistant.components.heatmiser heatmiserV3==1.1.18 @@ -1084,13 +1084,13 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.50 +holidays==0.51 # homeassistant.components.frontend home-assistant-frontend==20240610.1 # homeassistant.components.conversation -home-assistant-intents==2024.6.5 +home-assistant-intents==2024.6.21 # homeassistant.components.home_connect homeconnect==0.7.2 @@ -1566,7 +1566,7 @@ plexauth==0.0.6 plexwebsocket==0.0.14 # homeassistant.components.plugwise -plugwise==0.37.3 +plugwise==0.37.4.1 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 @@ -1619,7 +1619,7 @@ pvo==2.1.1 py-aosmith==1.0.8 # homeassistant.components.canary -py-canary==0.5.3 +py-canary==0.5.4 # homeassistant.components.ccm15 py-ccm15==0.0.9 @@ -1794,7 +1794,7 @@ pydiscovergy==3.0.1 pydoods==1.0.2 # homeassistant.components.hydrawise -pydrawise==2024.6.3 +pydrawise==2024.6.4 # homeassistant.components.android_ip_webcam pydroid-ipcam==2.0.0 @@ -2867,7 +2867,7 @@ watchdog==2.3.1 waterfurnace==1.1.0 # homeassistant.components.weatherflow_cloud -weatherflow4py==0.2.20 +weatherflow4py==0.2.21 # homeassistant.components.webmin webmin-xmlrpc==0.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6bf487f7ef9..328f660547f 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.1 +AEMET-OpenData==0.5.2 # homeassistant.components.honeywell AIOSomecomfort==0.0.25 @@ -237,7 +237,7 @@ aiohomekit==3.1.5 aiohue==4.7.1 # homeassistant.components.imap -aioimaplib==1.0.1 +aioimaplib==1.1.0 # homeassistant.components.apache_kafka aiokafka==0.10.0 @@ -463,7 +463,7 @@ azure-kusto-data[aio]==3.1.0 azure-kusto-ingest==3.1.0 # homeassistant.components.holiday -babel==2.13.1 +babel==2.15.0 # homeassistant.components.homekit base36==0.1.1 @@ -664,7 +664,7 @@ energyzero==2.1.0 enocean==0.50 # homeassistant.components.environment_canada -env-canada==0.6.2 +env-canada==0.6.3 # homeassistant.components.season ephem==4.1.5 @@ -867,7 +867,7 @@ hass-nabucasa==0.81.1 hassil==1.7.1 # homeassistant.components.jewish_calendar -hdate==0.10.8 +hdate==0.10.9 # homeassistant.components.here_travel_time here-routing==0.2.0 @@ -886,13 +886,13 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.50 +holidays==0.51 # homeassistant.components.frontend home-assistant-frontend==20240610.1 # homeassistant.components.conversation -home-assistant-intents==2024.6.5 +home-assistant-intents==2024.6.21 # homeassistant.components.home_connect homeconnect==0.7.2 @@ -1243,7 +1243,7 @@ plexauth==0.0.6 plexwebsocket==0.0.14 # homeassistant.components.plugwise -plugwise==0.37.3 +plugwise==0.37.4.1 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 @@ -1284,7 +1284,7 @@ pvo==2.1.1 py-aosmith==1.0.8 # homeassistant.components.canary -py-canary==0.5.3 +py-canary==0.5.4 # homeassistant.components.ccm15 py-ccm15==0.0.9 @@ -1405,7 +1405,7 @@ pydexcom==0.2.3 pydiscovergy==3.0.1 # homeassistant.components.hydrawise -pydrawise==2024.6.3 +pydrawise==2024.6.4 # homeassistant.components.android_ip_webcam pydroid-ipcam==2.0.0 @@ -2226,7 +2226,7 @@ wallbox==0.6.0 watchdog==2.3.1 # homeassistant.components.weatherflow_cloud -weatherflow4py==0.2.20 +weatherflow4py==0.2.21 # homeassistant.components.webmin webmin-xmlrpc==0.0.2 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 1f2f4bcab66..a12decd5b2c 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -219,6 +219,9 @@ scapy>=2.5.0 # Only tuf>=4 includes a constraint to <1.0. # https://github.com/theupdateframework/python-tuf/releases/tag/v4.0.0 tuf>=4.0.0 + +# https://github.com/jd/tenacity/issues/471 +tenacity<8.4.0 """ GENERATED_MESSAGE = ( diff --git a/tests/components/conversation/snapshots/test_init.ambr b/tests/components/conversation/snapshots/test_init.ambr index 6264e61863f..403c72aaa10 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 light', + 'speech': 'Sorry, I am not aware of any device called kitchen', }), }), }), @@ -703,7 +703,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device called late added light', + 'speech': 'Sorry, I am not aware of any device called late added', }), }), }), @@ -783,7 +783,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device called kitchen light', + 'speech': 'Sorry, I am not aware of any device called kitchen', }), }), }), @@ -803,7 +803,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device called my cool light', + 'speech': 'Sorry, I am not aware of any device called my cool', }), }), }), @@ -943,7 +943,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device called kitchen light', + 'speech': 'Sorry, I am not aware of any device called kitchen', }), }), }), @@ -993,7 +993,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device called renamed light', + 'speech': 'Sorry, I am not aware of any device called renamed', }), }), }), diff --git a/tests/components/hydrawise/conftest.py b/tests/components/hydrawise/conftest.py index 550e944db36..e1d0db47ebc 100644 --- a/tests/components/hydrawise/conftest.py +++ b/tests/components/hydrawise/conftest.py @@ -15,6 +15,7 @@ from pydrawise.schema import ( Sensor, SensorModel, SensorStatus, + UnitsSummary, User, Zone, ) @@ -84,7 +85,11 @@ def mock_auth() -> Generator[AsyncMock, None, None]: @pytest.fixture def user() -> User: """Hydrawise User fixture.""" - return User(customer_id=12345, email="asdf@asdf.com") + return User( + customer_id=12345, + email="asdf@asdf.com", + units=UnitsSummary(units_name="imperial"), + ) @pytest.fixture diff --git a/tests/components/hydrawise/test_sensor.py b/tests/components/hydrawise/test_sensor.py index fcbc47c41f4..af75ad69ade 100644 --- a/tests/components/hydrawise/test_sensor.py +++ b/tests/components/hydrawise/test_sensor.py @@ -3,13 +3,18 @@ from collections.abc import Awaitable, Callable from unittest.mock import patch -from pydrawise.schema import Controller, Zone +from pydrawise.schema import Controller, User, Zone import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from homeassistant.util.unit_system import ( + METRIC_SYSTEM, + US_CUSTOMARY_SYSTEM, + UnitSystem, +) from tests.common import MockConfigEntry, snapshot_platform @@ -45,7 +50,7 @@ async def test_suspended_state( assert next_cycle.state == "unknown" -async def test_no_sensor_and_water_state2( +async def test_no_sensor_and_water_state( hass: HomeAssistant, controller: Controller, mock_add_config_entry: Callable[[], Awaitable[MockConfigEntry]], @@ -63,3 +68,30 @@ async def test_no_sensor_and_water_state2( sensor = hass.states.get("binary_sensor.home_controller_connectivity") assert sensor is not None assert sensor.state == "on" + + +@pytest.mark.parametrize( + ("hydrawise_unit_system", "unit_system", "expected_state"), + [ + ("imperial", METRIC_SYSTEM, "454.6279552584"), + ("imperial", US_CUSTOMARY_SYSTEM, "120.1"), + ("metric", METRIC_SYSTEM, "120.1"), + ("metric", US_CUSTOMARY_SYSTEM, "31.7270634882136"), + ], +) +async def test_volume_unit_conversion( + hass: HomeAssistant, + unit_system: UnitSystem, + hydrawise_unit_system: str, + expected_state: str, + user: User, + mock_add_config_entry: Callable[[], Awaitable[MockConfigEntry]], +) -> None: + """Test volume unit conversion.""" + hass.config.units = unit_system + user.units.units_name = hydrawise_unit_system + await mock_add_config_entry() + + daily_active_water_use = hass.states.get("sensor.zone_one_daily_active_water_use") + assert daily_active_water_use is not None + assert daily_active_water_use.state == expected_state diff --git a/tests/components/jewish_calendar/test_init.py b/tests/components/jewish_calendar/test_init.py index f052d4e7f46..b8454b41a60 100644 --- a/tests/components/jewish_calendar/test_init.py +++ b/tests/components/jewish_calendar/test_init.py @@ -38,7 +38,6 @@ async def test_import_unique_id_migration(hass: HomeAssistant) -> None: latitude=yaml_conf[DOMAIN][CONF_LATITUDE], longitude=yaml_conf[DOMAIN][CONF_LONGITUDE], timezone=hass.config.time_zone, - altitude=hass.config.elevation, diaspora=DEFAULT_DIASPORA, ) old_prefix = get_unique_prefix(location, DEFAULT_LANGUAGE, 20, 50) diff --git a/tests/components/songpal/__init__.py b/tests/components/songpal/__init__.py index ab585c5a6d5..15bf0c530d3 100644 --- a/tests/components/songpal/__init__.py +++ b/tests/components/songpal/__init__.py @@ -23,7 +23,9 @@ CONF_DATA = { } -def _create_mocked_device(throw_exception=False, wired_mac=MAC, wireless_mac=None): +def _create_mocked_device( + throw_exception=False, wired_mac=MAC, wireless_mac=None, no_soundfield=False +): mocked_device = MagicMock() type(mocked_device).get_supported_methods = AsyncMock( @@ -101,7 +103,14 @@ def _create_mocked_device(throw_exception=False, wired_mac=MAC, wireless_mac=Non soundField = MagicMock() soundField.currentValue = "sound_mode2" soundField.candidate = [sound_mode1, sound_mode2, sound_mode3] - type(mocked_device).get_sound_settings = AsyncMock(return_value=[soundField]) + + settings = MagicMock() + settings.target = "soundField" + settings.__iter__.return_value = [soundField] + + type(mocked_device).get_sound_settings = AsyncMock( + return_value=[] if no_soundfield else [settings] + ) type(mocked_device).set_power = AsyncMock() type(mocked_device).set_sound_settings = AsyncMock() diff --git a/tests/components/songpal/test_media_player.py b/tests/components/songpal/test_media_player.py index 2393a5a9086..8f56170b839 100644 --- a/tests/components/songpal/test_media_player.py +++ b/tests/components/songpal/test_media_player.py @@ -159,6 +159,43 @@ async def test_state( assert entity.unique_id == MAC +async def test_state_nosoundmode( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, +) -> None: + """Test state of the entity with no soundField in sound settings.""" + mocked_device = _create_mocked_device(no_soundfield=True) + entry = MockConfigEntry(domain=songpal.DOMAIN, data=CONF_DATA) + entry.add_to_hass(hass) + + with _patch_media_player_device(mocked_device): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.name == FRIENDLY_NAME + assert state.state == STATE_ON + attributes = state.as_dict()["attributes"] + assert attributes["volume_level"] == 0.5 + assert attributes["is_volume_muted"] is False + assert attributes["source_list"] == ["title1", "title2"] + assert attributes["source"] == "title2" + assert "sound_mode_list" not in attributes + assert "sound_mode" not in attributes + assert attributes["supported_features"] == SUPPORT_SONGPAL + + device = device_registry.async_get_device(identifiers={(songpal.DOMAIN, MAC)}) + assert device.connections == {(dr.CONNECTION_NETWORK_MAC, MAC)} + assert device.manufacturer == "Sony Corporation" + assert device.name == FRIENDLY_NAME + assert device.sw_version == SW_VERSION + assert device.model == MODEL + + entity = entity_registry.async_get(ENTITY_ID) + assert entity.unique_id == MAC + + async def test_state_wireless( hass: HomeAssistant, device_registry: dr.DeviceRegistry, diff --git a/tests/components/synology_dsm/test_media_source.py b/tests/components/synology_dsm/test_media_source.py index 2a792d174f8..433a4b15c23 100644 --- a/tests/components/synology_dsm/test_media_source.py +++ b/tests/components/synology_dsm/test_media_source.py @@ -50,7 +50,8 @@ def dsm_with_photos() -> MagicMock: 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", False), + SynoPhotosItem(10, "", "filename.jpg", 12345, "10_1298753", "sm", True), ] ) dsm.photos.get_item_thumbnail_url = AsyncMock( @@ -102,6 +103,11 @@ async def test_resolve_media_bad_identifier( "/synology_dsm/ABC012345/12631_47189/filename.png", "image/png", ), + ( + "ABC012345/12/12631_47189/filename.png_shared", + "/synology_dsm/ABC012345/12631_47189/filename.png_shared", + "image/png", + ), ], ) async def test_resolve_media_success( @@ -333,7 +339,7 @@ async def test_browse_media_get_items_thumbnail_error( result = await source.async_browse_media(item) assert result - assert len(result.children) == 1 + assert len(result.children) == 2 item = result.children[0] assert isinstance(item, BrowseMedia) assert item.thumbnail is None @@ -372,7 +378,7 @@ async def test_browse_media_get_items( result = await source.async_browse_media(item) assert result - assert len(result.children) == 1 + 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" @@ -382,6 +388,15 @@ async def test_browse_media_get_items( assert item.can_play assert not item.can_expand 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.title == "filename.jpg" + assert item.media_class == MediaClass.IMAGE + assert item.media_content_type == "image/jpeg" + assert item.can_play + assert not item.can_expand + assert item.thumbnail == "http://my.thumbnail.url" @pytest.mark.usefixtures("setup_media_source") @@ -435,3 +450,8 @@ async def test_media_view( 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" + ) + assert isinstance(result, web.Response)