mirror of
https://github.com/home-assistant/core.git
synced 2026-01-06 15:47:19 +01:00
Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b315b566e5 | ||
|
|
febcb33545 | ||
|
|
e62268d5ea | ||
|
|
c1dc6fd511 | ||
|
|
92c12fdf0a | ||
|
|
53a21dcb6b | ||
|
|
59b2f4e56f | ||
|
|
75a469f4d6 | ||
|
|
39f67afa64 | ||
|
|
5b322f1af5 | ||
|
|
5edf480a15 | ||
|
|
96adf98625 | ||
|
|
500ef94ad4 | ||
|
|
a55e82366a | ||
|
|
c3607bd6d5 | ||
|
|
16314c5c7c | ||
|
|
91064697b5 | ||
|
|
5b1b137fd2 | ||
|
|
e1225d3f56 | ||
|
|
99d1de901e | ||
|
|
08578147f5 | ||
|
|
0b4bbbffc8 | ||
|
|
98aeb0b034 | ||
|
|
7a9537dcc9 | ||
|
|
8e63bd3ac0 | ||
|
|
ddec6d04e1 | ||
|
|
94800cb11e | ||
|
|
394dafd980 | ||
|
|
eba429dc54 | ||
|
|
89ce8478de | ||
|
|
a4a8315376 | ||
|
|
3a705fd668 | ||
|
|
dc0fc318b8 | ||
|
|
5ceb8537eb | ||
|
|
d7d7782a69 | ||
|
|
2d4176d581 | ||
|
|
204e9a79c5 | ||
|
|
ace7da2328 | ||
|
|
dfe25ff804 | ||
|
|
2b44cf898e | ||
|
|
c77ed921de | ||
|
|
78e13d138f | ||
|
|
4e394597bd | ||
|
|
78c2dc708c | ||
|
|
4c1d2e7ac8 | ||
|
|
7b809a8e55 | ||
|
|
4eea448f9d | ||
|
|
f58882c878 | ||
|
|
4e6e9f35b5 | ||
|
|
d5e9976b2c | ||
|
|
8d547d4599 | ||
|
|
94d79440a0 | ||
|
|
d602b7d19b | ||
|
|
fb5de55c3e | ||
|
|
5cf0ee936d | ||
|
|
7443878333 |
2
.github/workflows/builder.yml
vendored
2
.github/workflows/builder.yml
vendored
@@ -10,7 +10,7 @@ on:
|
||||
|
||||
env:
|
||||
BUILD_TYPE: core
|
||||
DEFAULT_PYTHON: "3.12"
|
||||
DEFAULT_PYTHON: "3.12.3"
|
||||
PIP_TIMEOUT: 60
|
||||
UV_HTTP_TIMEOUT: 60
|
||||
UV_SYSTEM_PYTHON: "true"
|
||||
|
||||
4
.github/workflows/ci.yaml
vendored
4
.github/workflows/ci.yaml
vendored
@@ -37,8 +37,8 @@ env:
|
||||
UV_CACHE_VERSION: 1
|
||||
MYPY_CACHE_VERSION: 8
|
||||
HA_SHORT_VERSION: "2024.6"
|
||||
DEFAULT_PYTHON: "3.12"
|
||||
ALL_PYTHON_VERSIONS: "['3.12']"
|
||||
DEFAULT_PYTHON: "3.12.3"
|
||||
ALL_PYTHON_VERSIONS: "['3.12.3']"
|
||||
# 10.3 is the oldest supported version
|
||||
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
|
||||
# 10.6 is the current long-term-support
|
||||
|
||||
2
.github/workflows/translations.yml
vendored
2
.github/workflows/translations.yml
vendored
@@ -10,7 +10,7 @@ on:
|
||||
- "**strings.json"
|
||||
|
||||
env:
|
||||
DEFAULT_PYTHON: "3.11"
|
||||
DEFAULT_PYTHON: "3.12.3"
|
||||
|
||||
jobs:
|
||||
upload:
|
||||
|
||||
2
.github/workflows/wheels.yml
vendored
2
.github/workflows/wheels.yml
vendored
@@ -17,7 +17,7 @@ on:
|
||||
- "script/gen_requirements_all.py"
|
||||
|
||||
env:
|
||||
DEFAULT_PYTHON: "3.12"
|
||||
DEFAULT_PYTHON: "3.12.3"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref_name}}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/buienradar",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["buienradar", "vincenty"],
|
||||
"requirements": ["buienradar==1.0.5"]
|
||||
"requirements": ["buienradar==1.0.6"]
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -86,6 +86,7 @@ class Concord232Alarm(AlarmControlPanelEntity):
|
||||
|
||||
self._attr_name = name
|
||||
self._code = code
|
||||
self._alarm_control_panel_option_default_code = code
|
||||
self._mode = mode
|
||||
self._url = url
|
||||
self._alarm = concord232_client.Client(self._url)
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20240610.0"]
|
||||
"requirements": ["home-assistant-frontend==20240610.1"]
|
||||
}
|
||||
|
||||
@@ -13,5 +13,6 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/gardena_bluetooth",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["bleak", "bleak_esphome", "gardena_bluetooth"],
|
||||
"requirements": ["gardena-bluetooth==1.4.2"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/goodwe",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["goodwe"],
|
||||
"requirements": ["goodwe==0.3.5"]
|
||||
"requirements": ["goodwe==0.3.6"]
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
"after_dependencies": [
|
||||
"alarm_control_panel",
|
||||
"climate",
|
||||
"cover",
|
||||
"device_tracker",
|
||||
"lock",
|
||||
"media_player",
|
||||
"person",
|
||||
"plant",
|
||||
"vacuum",
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,7 +9,6 @@ from typing import TYPE_CHECKING, Any
|
||||
from icmplib import NameLookupError, async_ping
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import UpdateFailed
|
||||
|
||||
from .const import ICMP_TIMEOUT, PING_TIMEOUT
|
||||
|
||||
@@ -59,9 +58,10 @@ class PingDataICMPLib(PingData):
|
||||
timeout=ICMP_TIMEOUT,
|
||||
privileged=self._privileged,
|
||||
)
|
||||
except NameLookupError as err:
|
||||
except NameLookupError:
|
||||
_LOGGER.debug("Error resolving host: %s", self.ip_address)
|
||||
self.is_alive = False
|
||||
raise UpdateFailed(f"Error resolving host: {self.ip_address}") from err
|
||||
return
|
||||
|
||||
_LOGGER.debug(
|
||||
"async_ping returned: reachable=%s sent=%i received=%s",
|
||||
@@ -152,17 +152,22 @@ class PingDataSubProcess(PingData):
|
||||
if TYPE_CHECKING:
|
||||
assert match is not None
|
||||
rtt_min, rtt_avg, rtt_max, rtt_mdev = match.groups()
|
||||
except TimeoutError as err:
|
||||
except TimeoutError:
|
||||
_LOGGER.debug(
|
||||
"Timed out running command: `%s`, after: %s",
|
||||
" ".join(self._ping_cmd),
|
||||
self._count + PING_TIMEOUT,
|
||||
)
|
||||
|
||||
if pinger:
|
||||
with suppress(TypeError):
|
||||
await pinger.kill() # type: ignore[func-returns-value]
|
||||
del pinger
|
||||
|
||||
raise UpdateFailed(
|
||||
f"Timed out running command: `{self._ping_cmd}`, after: {self._count + PING_TIMEOUT}s"
|
||||
) from err
|
||||
return None
|
||||
except AttributeError as err:
|
||||
raise UpdateFailed from err
|
||||
_LOGGER.debug("Error matching ping output: %s", err)
|
||||
return None
|
||||
return {"min": rtt_min, "avg": rtt_avg, "max": rtt_max, "mdev": rtt_mdev}
|
||||
|
||||
async def async_update(self) -> None:
|
||||
|
||||
@@ -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."]
|
||||
}
|
||||
|
||||
@@ -116,7 +116,6 @@ async def async_setup_entry(
|
||||
class ReolinkCamera(ReolinkChannelCoordinatorEntity, Camera):
|
||||
"""An implementation of a Reolink IP camera."""
|
||||
|
||||
_attr_supported_features: CameraEntityFeature = CameraEntityFeature.STREAM
|
||||
entity_description: ReolinkCameraEntityDescription
|
||||
|
||||
def __init__(
|
||||
@@ -130,6 +129,9 @@ class ReolinkCamera(ReolinkChannelCoordinatorEntity, Camera):
|
||||
ReolinkChannelCoordinatorEntity.__init__(self, reolink_data, channel)
|
||||
Camera.__init__(self)
|
||||
|
||||
if "snapshots" not in entity_description.stream:
|
||||
self._attr_supported_features = CameraEntityFeature.STREAM
|
||||
|
||||
if self._host.api.model in DUAL_LENS_MODELS:
|
||||
self._attr_translation_key = (
|
||||
f"{entity_description.translation_key}_lens_{self._channel}"
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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__)
|
||||
|
||||
19
homeassistant/components/spotify/models.py
Normal file
19
homeassistant/components/spotify/models.py
Normal 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
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -37,7 +37,6 @@ from .const import (
|
||||
CONST_MODE_SMART_SCHEDULE,
|
||||
CONST_OVERLAY_MANUAL,
|
||||
CONST_OVERLAY_TADO_OPTIONS,
|
||||
CONST_OVERLAY_TIMER,
|
||||
DATA,
|
||||
DOMAIN,
|
||||
HA_TERMINATION_DURATION,
|
||||
@@ -65,7 +64,7 @@ from .const import (
|
||||
TYPE_HEATING,
|
||||
)
|
||||
from .entity import TadoZoneEntity
|
||||
from .helper import decide_overlay_mode
|
||||
from .helper import decide_duration, decide_overlay_mode
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -603,14 +602,12 @@ class TadoClimate(TadoZoneEntity, ClimateEntity):
|
||||
overlay_mode=overlay_mode,
|
||||
zone_id=self.zone_id,
|
||||
)
|
||||
# If we ended up with a timer but no duration, set a default duration
|
||||
if overlay_mode == CONST_OVERLAY_TIMER and duration is None:
|
||||
duration = (
|
||||
int(self._tado_zone_data.default_overlay_termination_duration)
|
||||
if self._tado_zone_data.default_overlay_termination_duration is not None
|
||||
else 3600
|
||||
)
|
||||
|
||||
duration = decide_duration(
|
||||
tado=self._tado,
|
||||
duration=duration,
|
||||
zone_id=self.zone_id,
|
||||
overlay_mode=overlay_mode,
|
||||
)
|
||||
_LOGGER.debug(
|
||||
(
|
||||
"Switching to %s for zone %s (%d) with temperature %s °C and duration"
|
||||
|
||||
@@ -212,3 +212,5 @@ SERVICE_ADD_METER_READING = "add_meter_reading"
|
||||
CONF_CONFIG_ENTRY = "config_entry"
|
||||
CONF_READING = "reading"
|
||||
ATTR_MESSAGE = "message"
|
||||
|
||||
WATER_HEATER_FALLBACK_REPAIR = "water_heater_fallback"
|
||||
|
||||
@@ -29,3 +29,23 @@ def decide_overlay_mode(
|
||||
)
|
||||
|
||||
return overlay_mode
|
||||
|
||||
|
||||
def decide_duration(
|
||||
tado: TadoConnector,
|
||||
duration: int | None,
|
||||
zone_id: int,
|
||||
overlay_mode: str | None = None,
|
||||
) -> None | int:
|
||||
"""Return correct duration based on the selected overlay mode/duration and tado config."""
|
||||
# If we ended up with a timer but no duration, set a default duration
|
||||
# If we ended up with a timer but no duration, set a default duration
|
||||
if overlay_mode == CONST_OVERLAY_TIMER and duration is None:
|
||||
duration = (
|
||||
int(tado.data["zone"][zone_id].default_overlay_termination_duration)
|
||||
if tado.data["zone"][zone_id].default_overlay_termination_duration
|
||||
is not None
|
||||
else 3600
|
||||
)
|
||||
|
||||
return duration
|
||||
|
||||
34
homeassistant/components/tado/repairs.py
Normal file
34
homeassistant/components/tado/repairs.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""Repair implementations."""
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
|
||||
from .const import (
|
||||
CONST_OVERLAY_MANUAL,
|
||||
CONST_OVERLAY_TADO_DEFAULT,
|
||||
DOMAIN,
|
||||
WATER_HEATER_FALLBACK_REPAIR,
|
||||
)
|
||||
|
||||
|
||||
def manage_water_heater_fallback_issue(
|
||||
hass: HomeAssistant,
|
||||
water_heater_entities: list,
|
||||
integration_overlay_fallback: str | None,
|
||||
) -> None:
|
||||
"""Notify users about water heater respecting fallback setting."""
|
||||
if (
|
||||
integration_overlay_fallback
|
||||
in [CONST_OVERLAY_TADO_DEFAULT, CONST_OVERLAY_MANUAL]
|
||||
and len(water_heater_entities) > 0
|
||||
):
|
||||
for water_heater_entity in water_heater_entities:
|
||||
ir.async_create_issue(
|
||||
hass=hass,
|
||||
domain=DOMAIN,
|
||||
issue_id=f"{WATER_HEATER_FALLBACK_REPAIR}_{water_heater_entity.zone_name}",
|
||||
is_fixable=False,
|
||||
is_persistent=False,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key=WATER_HEATER_FALLBACK_REPAIR,
|
||||
)
|
||||
@@ -165,6 +165,10 @@
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,8 @@ from .const import (
|
||||
TYPE_HOT_WATER,
|
||||
)
|
||||
from .entity import TadoZoneEntity
|
||||
from .helper import decide_overlay_mode
|
||||
from .helper import decide_duration, decide_overlay_mode
|
||||
from .repairs import manage_water_heater_fallback_issue
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -80,6 +81,12 @@ async def async_setup_entry(
|
||||
|
||||
async_add_entities(entities, True)
|
||||
|
||||
manage_water_heater_fallback_issue(
|
||||
hass=hass,
|
||||
water_heater_entities=entities,
|
||||
integration_overlay_fallback=tado.fallback,
|
||||
)
|
||||
|
||||
|
||||
def _generate_entities(tado: TadoConnector) -> list[WaterHeaterEntity]:
|
||||
"""Create all water heater entities."""
|
||||
@@ -283,7 +290,12 @@ class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity):
|
||||
duration=duration,
|
||||
zone_id=self.zone_id,
|
||||
)
|
||||
|
||||
duration = decide_duration(
|
||||
tado=self._tado,
|
||||
duration=duration,
|
||||
zone_id=self.zone_id,
|
||||
overlay_mode=overlay_mode,
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"Switching to %s for zone %s (%d) with temperature %s",
|
||||
self._current_tado_hvac_mode,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -54,6 +54,10 @@ SCAN_INTERVAL = timedelta(seconds=DEFAULT_SCAN_INTERVAL)
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
EARLY_ACCESS_URL = (
|
||||
"https://www.home-assistant.io/integrations/unifiprotect#software-support"
|
||||
)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the UniFi Protect."""
|
||||
@@ -122,8 +126,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
DOMAIN,
|
||||
"ea_channel_warning",
|
||||
is_fixable=True,
|
||||
is_persistent=True,
|
||||
learn_more_url="https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access",
|
||||
is_persistent=False,
|
||||
learn_more_url=EARLY_ACCESS_URL,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="ea_channel_warning",
|
||||
translation_placeholders={"version": str(nvr_info.version)},
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["uiprotect", "unifi_discovery"],
|
||||
"requirements": ["uiprotect==0.4.1", "unifi-discovery==1.1.8"],
|
||||
"requirements": ["uiprotect==1.7.2", "unifi-discovery==1.1.8"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Ubiquiti Networks",
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
"all_updates": "Realtime metrics (WARNING: Greatly increases CPU usage)",
|
||||
"override_connection_host": "Override Connection Host",
|
||||
"max_media": "Max number of event to load for Media Browser (increases RAM usage)",
|
||||
"allow_ea": "Allow Early Access versions of Protect (WARNING: Will mark your integration as unsupported)"
|
||||
"allow_ea_channel": "Allow Early Access versions of Protect (WARNING: Will mark your integration as unsupported)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,7 +67,7 @@
|
||||
"step": {
|
||||
"start": {
|
||||
"title": "UniFi Protect Early Access enabled",
|
||||
"description": "You are either running an Early Access version of UniFi Protect (v{version}) or opt-ed into a release channel that is not the Official Release Channel. [Home Assistant does not support Early Access versions](https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access), so you should immediately switch to the Official Release Channel. Accidentally upgrading to an Early Access version can break your UniFi Protect integration.\n\nBy submitting this form, you have switched back to the Official Release Channel or agree to run an unsupported version of UniFi Protect, which may break your Home Assistant integration at any time."
|
||||
"description": "You are either running an Early Access version of UniFi Protect (v{version}) or opt-ed into a release channel that is not the Official Release Channel.\n\nAs these Early Access releases may not be tested yet, using it may cause the UniFi Protect integration to behave unexpectedly. [Read more about Early Access and Home Assistant]({learn_more}).\n\nSubmit to dismiss this message."
|
||||
},
|
||||
"confirm": {
|
||||
"title": "[%key:component::unifiprotect::issues::ea_channel_warning::fix_flow::step::start::title%]",
|
||||
|
||||
@@ -89,10 +89,8 @@ def async_get_devices_by_type(
|
||||
bootstrap: Bootstrap, device_type: ModelType
|
||||
) -> dict[str, ProtectAdoptableDeviceModel]:
|
||||
"""Get devices by type."""
|
||||
|
||||
devices: dict[str, ProtectAdoptableDeviceModel] = getattr(
|
||||
bootstrap, f"{device_type.value}s"
|
||||
)
|
||||
devices: dict[str, ProtectAdoptableDeviceModel]
|
||||
devices = getattr(bootstrap, device_type.devices_key)
|
||||
return devices
|
||||
|
||||
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ async def _async_validate_country_and_province(
|
||||
DOMAIN,
|
||||
"bad_country",
|
||||
is_fixable=True,
|
||||
is_persistent=True,
|
||||
is_persistent=False,
|
||||
severity=IssueSeverity.ERROR,
|
||||
translation_key="bad_country",
|
||||
translation_placeholders={"title": entry.title},
|
||||
@@ -59,7 +59,7 @@ async def _async_validate_country_and_province(
|
||||
DOMAIN,
|
||||
"bad_province",
|
||||
is_fixable=True,
|
||||
is_persistent=True,
|
||||
is_persistent=False,
|
||||
severity=IssueSeverity.ERROR,
|
||||
translation_key="bad_province",
|
||||
translation_placeholders={
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["holidays"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["holidays==0.50"]
|
||||
"requirements": ["holidays==0.51"]
|
||||
}
|
||||
|
||||
@@ -21,11 +21,11 @@
|
||||
"universal_silabs_flasher"
|
||||
],
|
||||
"requirements": [
|
||||
"bellows==0.39.0",
|
||||
"bellows==0.39.1",
|
||||
"pyserial==3.5",
|
||||
"zha-quirks==0.0.116",
|
||||
"zigpy-deconz==0.23.1",
|
||||
"zigpy==0.64.0",
|
||||
"zigpy==0.64.1",
|
||||
"zigpy-xbee==0.20.1",
|
||||
"zigpy-zigate==0.12.0",
|
||||
"zigpy-znp==0.12.1",
|
||||
|
||||
@@ -24,7 +24,7 @@ if TYPE_CHECKING:
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2024
|
||||
MINOR_VERSION: Final = 6
|
||||
PATCH_VERSION: Final = "2"
|
||||
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)
|
||||
|
||||
@@ -1362,6 +1362,8 @@ class IntentResponse:
|
||||
|
||||
if self.reprompt:
|
||||
response_dict["reprompt"] = self.reprompt
|
||||
if self.speech_slots:
|
||||
response_dict["speech_slots"] = self.speech_slots
|
||||
|
||||
response_data: dict[str, Any] = {}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -32,8 +32,8 @@ habluetooth==3.1.1
|
||||
hass-nabucasa==0.81.1
|
||||
hassil==1.7.1
|
||||
home-assistant-bluetooth==1.12.0
|
||||
home-assistant-frontend==20240610.0
|
||||
home-assistant-intents==2024.6.5
|
||||
home-assistant-frontend==20240610.1
|
||||
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
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2024.6.2"
|
||||
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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -547,7 +547,7 @@ beautifulsoup4==4.12.3
|
||||
# beewi-smartclim==0.0.10
|
||||
|
||||
# homeassistant.components.zha
|
||||
bellows==0.39.0
|
||||
bellows==0.39.1
|
||||
|
||||
# homeassistant.components.bmw_connected_drive
|
||||
bimmer-connected[china]==0.15.3
|
||||
@@ -634,7 +634,7 @@ bthomehub5-devicelist==0.1.1
|
||||
btsmarthub-devicelist==0.2.3
|
||||
|
||||
# homeassistant.components.buienradar
|
||||
buienradar==1.0.5
|
||||
buienradar==1.0.6
|
||||
|
||||
# homeassistant.components.dhcp
|
||||
cached_ipaddress==0.3.0
|
||||
@@ -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
|
||||
@@ -961,7 +961,7 @@ glances-api==0.8.0
|
||||
goalzero==0.2.2
|
||||
|
||||
# homeassistant.components.goodwe
|
||||
goodwe==0.3.5
|
||||
goodwe==0.3.6
|
||||
|
||||
# homeassistant.components.google_mail
|
||||
# homeassistant.components.google_tasks
|
||||
@@ -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.0
|
||||
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
|
||||
@@ -2779,7 +2779,7 @@ twitchAPI==4.0.0
|
||||
uasiren==0.0.1
|
||||
|
||||
# homeassistant.components.unifiprotect
|
||||
uiprotect==0.4.1
|
||||
uiprotect==1.7.2
|
||||
|
||||
# homeassistant.components.landisgyr_heat_meter
|
||||
ultraheat-api==0.5.7
|
||||
@@ -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
|
||||
@@ -2981,7 +2981,7 @@ zigpy-zigate==0.12.0
|
||||
zigpy-znp==0.12.1
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy==0.64.0
|
||||
zigpy==0.64.1
|
||||
|
||||
# homeassistant.components.zoneminder
|
||||
zm-py==0.5.4
|
||||
|
||||
@@ -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
|
||||
@@ -472,7 +472,7 @@ base36==0.1.1
|
||||
beautifulsoup4==4.12.3
|
||||
|
||||
# homeassistant.components.zha
|
||||
bellows==0.39.0
|
||||
bellows==0.39.1
|
||||
|
||||
# homeassistant.components.bmw_connected_drive
|
||||
bimmer-connected[china]==0.15.3
|
||||
@@ -536,7 +536,7 @@ brunt==1.2.0
|
||||
bthome-ble==3.8.1
|
||||
|
||||
# homeassistant.components.buienradar
|
||||
buienradar==1.0.5
|
||||
buienradar==1.0.6
|
||||
|
||||
# homeassistant.components.dhcp
|
||||
cached_ipaddress==0.3.0
|
||||
@@ -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
|
||||
@@ -790,7 +790,7 @@ glances-api==0.8.0
|
||||
goalzero==0.2.2
|
||||
|
||||
# homeassistant.components.goodwe
|
||||
goodwe==0.3.5
|
||||
goodwe==0.3.6
|
||||
|
||||
# homeassistant.components.google_mail
|
||||
# homeassistant.components.google_tasks
|
||||
@@ -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.0
|
||||
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
|
||||
@@ -2153,7 +2153,7 @@ twitchAPI==4.0.0
|
||||
uasiren==0.0.1
|
||||
|
||||
# homeassistant.components.unifiprotect
|
||||
uiprotect==0.4.1
|
||||
uiprotect==1.7.2
|
||||
|
||||
# homeassistant.components.landisgyr_heat_meter
|
||||
ultraheat-api==0.5.7
|
||||
@@ -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
|
||||
@@ -2322,7 +2322,7 @@ zigpy-zigate==0.12.0
|
||||
zigpy-znp==0.12.1
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy==0.64.0
|
||||
zigpy==0.64.1
|
||||
|
||||
# homeassistant.components.zwave_js
|
||||
zwave-js-server-python==0.56.0
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -9,7 +9,7 @@ from homeassistant.components.tado.const import (
|
||||
CONST_OVERLAY_TADO_MODE,
|
||||
CONST_OVERLAY_TIMER,
|
||||
)
|
||||
from homeassistant.components.tado.helper import decide_overlay_mode
|
||||
from homeassistant.components.tado.helper import decide_duration, decide_overlay_mode
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ def dummy_tado_connector(hass: HomeAssistant, fallback) -> TadoConnector:
|
||||
async def test_overlay_mode_duration_set(hass: HomeAssistant) -> None:
|
||||
"""Test overlay method selection when duration is set."""
|
||||
tado = dummy_tado_connector(hass=hass, fallback=CONST_OVERLAY_TADO_MODE)
|
||||
overlay_mode = decide_overlay_mode(tado=tado, duration="01:00:00", zone_id=1)
|
||||
overlay_mode = decide_overlay_mode(tado=tado, duration=3600, zone_id=1)
|
||||
# Must select TIMER overlay
|
||||
assert overlay_mode == CONST_OVERLAY_TIMER
|
||||
|
||||
@@ -52,3 +52,36 @@ async def test_overlay_mode_tado_default_fallback(hass: HomeAssistant) -> None:
|
||||
overlay_mode = decide_overlay_mode(tado=tado, duration=None, zone_id=zone_id)
|
||||
# Must fallback to zone setting
|
||||
assert overlay_mode == zone_fallback
|
||||
|
||||
|
||||
async def test_duration_enabled_without_tado_default(hass: HomeAssistant) -> None:
|
||||
"""Test duration decide method when overlay is timer and duration is set."""
|
||||
overlay = CONST_OVERLAY_TIMER
|
||||
expected_duration = 600
|
||||
tado = dummy_tado_connector(hass=hass, fallback=CONST_OVERLAY_MANUAL)
|
||||
duration = decide_duration(
|
||||
tado=tado, duration=expected_duration, overlay_mode=overlay, zone_id=0
|
||||
)
|
||||
# Should return the same duration value
|
||||
assert duration == expected_duration
|
||||
|
||||
|
||||
async def test_duration_enabled_with_tado_default(hass: HomeAssistant) -> None:
|
||||
"""Test overlay method selection when ended up with timer overlay and None duration."""
|
||||
zone_fallback = CONST_OVERLAY_TIMER
|
||||
expected_duration = 45000
|
||||
tado = dummy_tado_connector(hass=hass, fallback=zone_fallback)
|
||||
|
||||
class MockZoneData:
|
||||
def __init__(self) -> None:
|
||||
self.default_overlay_termination_duration = expected_duration
|
||||
|
||||
zone_id = 1
|
||||
|
||||
zone_data = {"zone": {zone_id: MockZoneData()}}
|
||||
with patch.dict(tado.data, zone_data):
|
||||
duration = decide_duration(
|
||||
tado=tado, duration=None, zone_id=zone_id, overlay_mode=zone_fallback
|
||||
)
|
||||
# Must fallback to zone timer setting
|
||||
assert duration == expected_duration
|
||||
|
||||
64
tests/components/tado/test_repairs.py
Normal file
64
tests/components/tado/test_repairs.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""Repair tests."""
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.tado.const import (
|
||||
CONST_OVERLAY_MANUAL,
|
||||
CONST_OVERLAY_TADO_DEFAULT,
|
||||
CONST_OVERLAY_TADO_MODE,
|
||||
DOMAIN,
|
||||
WATER_HEATER_FALLBACK_REPAIR,
|
||||
)
|
||||
from homeassistant.components.tado.repairs import manage_water_heater_fallback_issue
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
|
||||
|
||||
class MockWaterHeater:
|
||||
"""Mock Water heater entity."""
|
||||
|
||||
def __init__(self, zone_name) -> None:
|
||||
"""Init mock entity class."""
|
||||
self.zone_name = zone_name
|
||||
|
||||
|
||||
async def test_manage_water_heater_fallback_issue_not_created(
|
||||
hass: HomeAssistant,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
) -> None:
|
||||
"""Test water heater fallback issue is not needed."""
|
||||
zone_name = "Hot Water"
|
||||
expected_issue_id = f"{WATER_HEATER_FALLBACK_REPAIR}_{zone_name}"
|
||||
water_heater_entities = [MockWaterHeater(zone_name)]
|
||||
manage_water_heater_fallback_issue(
|
||||
water_heater_entities=water_heater_entities,
|
||||
integration_overlay_fallback=CONST_OVERLAY_TADO_MODE,
|
||||
hass=hass,
|
||||
)
|
||||
assert (
|
||||
issue_registry.async_get_issue(issue_id=expected_issue_id, domain=DOMAIN)
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"integration_overlay_fallback", [CONST_OVERLAY_TADO_DEFAULT, CONST_OVERLAY_MANUAL]
|
||||
)
|
||||
async def test_manage_water_heater_fallback_issue_created(
|
||||
hass: HomeAssistant,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
integration_overlay_fallback: str,
|
||||
) -> None:
|
||||
"""Test water heater fallback issue created cases."""
|
||||
zone_name = "Hot Water"
|
||||
expected_issue_id = f"{WATER_HEATER_FALLBACK_REPAIR}_{zone_name}"
|
||||
water_heater_entities = [MockWaterHeater(zone_name)]
|
||||
manage_water_heater_fallback_issue(
|
||||
water_heater_entities=water_heater_entities,
|
||||
integration_overlay_fallback=integration_overlay_fallback,
|
||||
hass=hass,
|
||||
)
|
||||
assert (
|
||||
issue_registry.async_get_issue(issue_id=expected_issue_id, domain=DOMAIN)
|
||||
is not None
|
||||
)
|
||||
@@ -61,7 +61,7 @@ async def test_ea_warning_ignore(
|
||||
|
||||
flow_id = data["flow_id"]
|
||||
assert data["description_placeholders"] == {
|
||||
"learn_more": "https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access",
|
||||
"learn_more": "https://www.home-assistant.io/integrations/unifiprotect#software-support",
|
||||
"version": str(version),
|
||||
}
|
||||
assert data["step_id"] == "start"
|
||||
@@ -73,7 +73,7 @@ async def test_ea_warning_ignore(
|
||||
|
||||
flow_id = data["flow_id"]
|
||||
assert data["description_placeholders"] == {
|
||||
"learn_more": "https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access",
|
||||
"learn_more": "https://www.home-assistant.io/integrations/unifiprotect#software-support",
|
||||
"version": str(version),
|
||||
}
|
||||
assert data["step_id"] == "confirm"
|
||||
@@ -123,7 +123,7 @@ async def test_ea_warning_fix(
|
||||
|
||||
flow_id = data["flow_id"]
|
||||
assert data["description_placeholders"] == {
|
||||
"learn_more": "https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access",
|
||||
"learn_more": "https://www.home-assistant.io/integrations/unifiprotect#software-support",
|
||||
"version": str(version),
|
||||
}
|
||||
assert data["step_id"] == "start"
|
||||
|
||||
@@ -149,8 +149,13 @@ async def test_assist_api(
|
||||
|
||||
assert test_context.json_fragment # To reproduce an error case in tracing
|
||||
intent_response = intent.IntentResponse("*")
|
||||
intent_response.matched_states = [State("light.matched", "on")]
|
||||
intent_response.unmatched_states = [State("light.unmatched", "on")]
|
||||
intent_response.async_set_states(
|
||||
[State("light.matched", "on")], [State("light.unmatched", "on")]
|
||||
)
|
||||
intent_response.async_set_speech("Some speech")
|
||||
intent_response.async_set_card("Card title", "card content")
|
||||
intent_response.async_set_speech_slots({"hello": 1})
|
||||
intent_response.async_set_reprompt("Do it again")
|
||||
tool_input = llm.ToolInput(
|
||||
tool_name="test_intent",
|
||||
tool_args={"area": "kitchen", "floor": "ground_floor"},
|
||||
@@ -181,8 +186,22 @@ async def test_assist_api(
|
||||
"success": [],
|
||||
"targets": [],
|
||||
},
|
||||
"reprompt": {
|
||||
"plain": {
|
||||
"extra_data": None,
|
||||
"reprompt": "Do it again",
|
||||
},
|
||||
},
|
||||
"response_type": "action_done",
|
||||
"speech": {},
|
||||
"speech": {
|
||||
"plain": {
|
||||
"extra_data": None,
|
||||
"speech": "Some speech",
|
||||
},
|
||||
},
|
||||
"speech_slots": {
|
||||
"hello": 1,
|
||||
},
|
||||
}
|
||||
|
||||
# Call with a device/area/floor
|
||||
@@ -227,7 +246,21 @@ async def test_assist_api(
|
||||
"targets": [],
|
||||
},
|
||||
"response_type": "action_done",
|
||||
"speech": {},
|
||||
"reprompt": {
|
||||
"plain": {
|
||||
"extra_data": None,
|
||||
"reprompt": "Do it again",
|
||||
},
|
||||
},
|
||||
"speech": {
|
||||
"plain": {
|
||||
"extra_data": None,
|
||||
"speech": "Some speech",
|
||||
},
|
||||
},
|
||||
"speech_slots": {
|
||||
"hello": 1,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user