pull/120249/head 2024.6.4
Franck Nijhof 2024-06-21 20:31:04 +02:00 committed by GitHub
commit b315b566e5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 277 additions and 111 deletions

View File

@ -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

View File

@ -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"]
}

View File

@ -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"]
}

View File

@ -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"]
}

View File

@ -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

View File

@ -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"]
}

View File

@ -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"]
}

View File

@ -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"]
}

View File

@ -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."""

View File

@ -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"]
}

View File

@ -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,

View File

@ -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
}

View File

@ -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"),
)

View File

@ -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

View File

@ -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."]
}

View File

@ -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]

View File

@ -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]

View File

@ -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

View File

@ -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__)

View File

@ -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

View File

@ -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"

View File

@ -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:

View File

@ -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

View File

@ -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"]
}

View File

@ -7,5 +7,5 @@
"iot_class": "local_polling",
"loggers": ["holidays"],
"quality_scale": "internal",
"requirements": ["holidays==0.50"]
"requirements": ["holidays==0.51"]
}

View File

@ -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)

View File

@ -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

View File

@ -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",

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 = (

View File

@ -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',
}),
}),
}),

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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()

View File

@ -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,

View File

@ -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)