mirror of
https://github.com/home-assistant/core.git
synced 2026-06-29 01:55:20 +02:00
Compare commits
65 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e62ff35fd | |||
| 0a5c1ef8eb | |||
| a88093afd2 | |||
| 9fd90283b3 | |||
| 3159242b68 | |||
| f5985b03e4 | |||
| 040a3bcb10 | |||
| 5aaf6704a9 | |||
| 2fcd00b301 | |||
| 0b439e6e4c | |||
| d13a5b7eec | |||
| de49716ec1 | |||
| 67c6921847 | |||
| 002b638013 | |||
| 4b60ed30c7 | |||
| 6f1deec507 | |||
| 227ba8032f | |||
| 7da3ecf033 | |||
| 8b293a18d3 | |||
| 3dc077f280 | |||
| d368a95323 | |||
| 495f41a742 | |||
| 9f7529706d | |||
| 8f6b1dff9c | |||
| f260a1bb7b | |||
| 157e137ea9 | |||
| b2e1a296d4 | |||
| e78a2c9f01 | |||
| 9011225a42 | |||
| 81ef9b99c2 | |||
| fa0207698a | |||
| 275883a95a | |||
| ebd252a225 | |||
| 2de6c0281d | |||
| f95671f0f4 | |||
| 5fcae9ecf7 | |||
| 0b86cfa496 | |||
| d45bdf37d5 | |||
| a9205df4a3 | |||
| c333744fd2 | |||
| 2f64601990 | |||
| cbd35be271 | |||
| 92ac14f42a | |||
| 45e568c73e | |||
| a121b8d146 | |||
| a2bd7d5857 | |||
| a6e639377b | |||
| 2147a851c3 | |||
| 9034afd29e | |||
| 5c5d259f63 | |||
| cc16a9086f | |||
| 5d1f8f770c | |||
| cea6b9b0b7 | |||
| 77f7c26399 | |||
| 8e0a5b258c | |||
| f8b942818c | |||
| 9660d12c77 | |||
| 7f1533a6e1 | |||
| 336d9e9126 | |||
| 1dde2d918e | |||
| 34a6b0ca61 | |||
| e92286ecd6 | |||
| 82bb9748db | |||
| 68e5e58a1c | |||
| f3e8403e9a |
@@ -37,6 +37,9 @@
|
||||
"title": "Re-authenticate AirVisual"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"type": "Integration type"
|
||||
},
|
||||
"description": "Pick what type of AirVisual data you want to monitor.",
|
||||
"title": "Configure AirVisual"
|
||||
}
|
||||
|
||||
@@ -190,8 +190,11 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
||||
await self._async_remove_device_stale(stale_devices)
|
||||
self.previous_devices = current_devices
|
||||
|
||||
current_routines = {slugify(routine) for routine in self.api.routines}
|
||||
if stale_routines := self.previous_routines - current_routines:
|
||||
current_routines = {
|
||||
f"{slugify(self.config_entry.unique_id)}-{slugify(routine)}"
|
||||
for routine in self.api.routines
|
||||
}
|
||||
if stale_routines := (self.previous_routines - current_routines):
|
||||
await self._async_remove_routine_stale(stale_routines)
|
||||
self.previous_routines = current_routines
|
||||
|
||||
@@ -225,17 +228,19 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
||||
"""Remove stale routine."""
|
||||
entity_registry = er.async_get(self.hass)
|
||||
|
||||
for routine in stale_routines:
|
||||
_LOGGER.debug(
|
||||
"Detected change in routines: routine %s removed",
|
||||
routine,
|
||||
)
|
||||
for routine_unique_id in stale_routines:
|
||||
entity_id = entity_registry.async_get_entity_id(
|
||||
Platform.BUTTON,
|
||||
DOMAIN,
|
||||
f"{slugify(self.config_entry.unique_id)}-{slugify(routine)}",
|
||||
routine_unique_id,
|
||||
)
|
||||
if entity_id:
|
||||
_LOGGER.debug(
|
||||
"Detected change in routines: routine %s removed",
|
||||
routine_unique_id.replace(
|
||||
f"{slugify(self.config_entry.unique_id)}-", ""
|
||||
),
|
||||
)
|
||||
entity_registry.async_remove(entity_id)
|
||||
|
||||
async def sync_history_state(self) -> None:
|
||||
|
||||
@@ -2,13 +2,20 @@
|
||||
|
||||
from typing import Final
|
||||
|
||||
from homeassistant.components.event import EventEntity, EventEntityDescription
|
||||
from aioamazondevices.const.devices import SPEAKER_GROUP_FAMILY
|
||||
|
||||
from homeassistant.components.event import (
|
||||
DOMAIN as EVENT_DOMAIN,
|
||||
EventEntity,
|
||||
EventEntityDescription,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import _LOGGER
|
||||
from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator
|
||||
from .entity import AmazonEntity
|
||||
from .utils import async_remove_entity_from_virtual_group
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
@@ -31,6 +38,11 @@ async def async_setup_entry(
|
||||
"""Set up Alexa Devices events based on a config entry."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
# Remove voice event from virtual groups
|
||||
await async_remove_entity_from_virtual_group(
|
||||
hass, coordinator, EVENT_DOMAIN, "voice_event"
|
||||
)
|
||||
|
||||
known_devices: set[str] = set()
|
||||
|
||||
def _check_device() -> None:
|
||||
@@ -42,6 +54,7 @@ async def async_setup_entry(
|
||||
AlexaVoiceEvent(coordinator, serial_num, event_desc)
|
||||
for event_desc in EVENTS
|
||||
for serial_num in new_devices
|
||||
if coordinator.data[serial_num].device_family != SPEAKER_GROUP_FAMILY
|
||||
)
|
||||
|
||||
_check_device()
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioamazondevices==14.0.3"]
|
||||
"requirements": ["aioamazondevices==14.1.3"]
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import AmazonConfigEntry, alexa_api_call
|
||||
from .entity import AmazonEntity
|
||||
from .utils import async_remove_dnd_from_virtual_group, async_update_unique_id
|
||||
from .utils import async_remove_entity_from_virtual_group, async_update_unique_id
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
@@ -58,7 +58,9 @@ async def async_setup_entry(
|
||||
new_key = "dnd"
|
||||
|
||||
# Remove old DND switch from virtual groups
|
||||
await async_remove_dnd_from_virtual_group(hass, coordinator, old_key)
|
||||
await async_remove_entity_from_virtual_group(
|
||||
hass, coordinator, SWITCH_DOMAIN, old_key
|
||||
)
|
||||
|
||||
# Replace unique id for DND switch
|
||||
await async_update_unique_id(hass, coordinator, SWITCH_DOMAIN, old_key, new_key)
|
||||
|
||||
@@ -8,7 +8,6 @@ from aioamazondevices.const.schedules import (
|
||||
)
|
||||
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.entity_registry as er
|
||||
|
||||
@@ -38,23 +37,22 @@ async def async_update_unique_id(
|
||||
entity_registry.async_update_entity(entity_id, new_unique_id=new_unique_id)
|
||||
|
||||
|
||||
async def async_remove_dnd_from_virtual_group(
|
||||
async def async_remove_entity_from_virtual_group(
|
||||
hass: HomeAssistant,
|
||||
coordinator: AmazonDevicesCoordinator,
|
||||
platform: str,
|
||||
key: str,
|
||||
) -> None:
|
||||
"""Remove entity DND from virtual group."""
|
||||
"""Remove entity from virtual group."""
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
for serial_num in coordinator.data:
|
||||
unique_id = f"{serial_num}-{key}"
|
||||
entity_id = entity_registry.async_get_entity_id(
|
||||
DOMAIN, SWITCH_DOMAIN, unique_id
|
||||
)
|
||||
entity_id = entity_registry.async_get_entity_id(DOMAIN, platform, unique_id)
|
||||
is_group = coordinator.data[serial_num].device_family == SPEAKER_GROUP_FAMILY
|
||||
if entity_id and is_group:
|
||||
entity_registry.async_remove(entity_id)
|
||||
_LOGGER.debug("Removed DND switch from virtual group %s", entity_id)
|
||||
_LOGGER.debug("Removed entity '%s' from virtual group", entity_id)
|
||||
|
||||
|
||||
async def async_remove_unsupported_notification_sensors(
|
||||
|
||||
@@ -34,11 +34,13 @@ def generate_site_selector_name(site: Site) -> str:
|
||||
|
||||
|
||||
def filter_sites(sites: list[Site]) -> list[Site]:
|
||||
"""Deduplicates the list of sites."""
|
||||
"""Filter out closed sites and deduplicate the list of sites."""
|
||||
filtered: list[Site] = []
|
||||
filtered_nmi: set[str] = set()
|
||||
|
||||
for site in sorted(sites, key=lambda site: site.status):
|
||||
if site.status == SiteStatus.CLOSED:
|
||||
continue
|
||||
if site.status == SiteStatus.ACTIVE or site.nmi not in filtered_nmi:
|
||||
filtered.append(site)
|
||||
filtered_nmi.add(site.nmi)
|
||||
|
||||
@@ -26,6 +26,12 @@
|
||||
"description": "The credentials for {username} need to be updated",
|
||||
"title": "Re-authenticate Blink"
|
||||
},
|
||||
"reconfigure": {
|
||||
"data": {
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"username": "[%key:common::config_flow::data::username%]"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aiocomelit"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiocomelit==2.0.3"]
|
||||
"requirements": ["aiocomelit==2.0.7"]
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"quality_scale": "internal",
|
||||
"requirements": [
|
||||
"aiodhcpwatcher==1.2.7",
|
||||
"aiodiscover==3.2.4",
|
||||
"cached-ipaddress==1.1.1"
|
||||
"aiodiscover==3.3.2",
|
||||
"cached-ipaddress==1.1.2"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["dsmr_parser"],
|
||||
"requirements": ["dsmr-parser==1.7.0"]
|
||||
"requirements": ["dsmr-parser==1.9.0"]
|
||||
}
|
||||
|
||||
@@ -30,7 +30,9 @@
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"username": "[%key:common::config_flow::data::username%]"
|
||||
},
|
||||
"description": "Please enter the API key obtained from ecobee.com."
|
||||
}
|
||||
|
||||
@@ -39,12 +39,12 @@ class EconetFanModeSelect(EcoNetEntity[Thermostat], SelectEntity):
|
||||
@property
|
||||
def options(self) -> list[str]:
|
||||
"""Return available select options."""
|
||||
return [e.value for e in self._econet.fan_modes]
|
||||
return [e.name for e in self._econet.fan_modes]
|
||||
|
||||
@property
|
||||
def current_option(self) -> str:
|
||||
"""Return current select option."""
|
||||
return self._econet.fan_mode.value
|
||||
return self._econet.fan_mode.name
|
||||
|
||||
def select_option(self, option: str) -> None:
|
||||
"""Set the selected option."""
|
||||
|
||||
@@ -246,8 +246,8 @@ class Elkm1ConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
if device is not None and device.mac_address:
|
||||
await self.async_set_unique_id(dr.format_mac(device.mac_address))
|
||||
# aborts if user tried to switch devices
|
||||
self._abort_if_unique_id_mismatch()
|
||||
if reconfigure_entry.unique_id is not None:
|
||||
self._abort_if_unique_id_mismatch()
|
||||
else:
|
||||
# If we cannot confirm identity, keep existing
|
||||
# behavior (don't block reconfigure)
|
||||
@@ -255,6 +255,7 @@ class Elkm1ConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
return self.async_update_reload_and_abort(
|
||||
reconfigure_entry,
|
||||
unique_id=self.unique_id,
|
||||
data_updates={
|
||||
**reconfigure_entry.data,
|
||||
CONF_HOST: info[CONF_HOST],
|
||||
|
||||
@@ -13,6 +13,11 @@
|
||||
"discovery_confirm": {
|
||||
"description": "Do you want to set up {model} {id} ({ipaddr})?"
|
||||
},
|
||||
"pick_device": {
|
||||
"data": {
|
||||
"device": "[%key:common::config_flow::data::device%]"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
|
||||
@@ -21,5 +21,5 @@
|
||||
"integration_type": "system",
|
||||
"preview_features": { "winter_mode": {} },
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20260527.6"]
|
||||
"requirements": ["home-assistant-frontend==20260527.7"]
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"device": "[%key:common::config_flow::data::device%]",
|
||||
"ip_address": "[%key:common::config_flow::data::ip%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"username": "[%key:common::config_flow::data::username%]"
|
||||
|
||||
@@ -189,7 +189,8 @@ class GrowattCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
) from err
|
||||
total_info["todayEnergy"] = total_info["today_energy"]
|
||||
total_info["totalEnergy"] = total_info["total_energy"]
|
||||
total_info["invTodayPpv"] = total_info["current_power"]
|
||||
# V1 API returns current_power in kW, convert to W
|
||||
total_info["invTodayPpv"] = total_info["current_power"] * 1000
|
||||
else:
|
||||
# Classic API: use plant_info as before.
|
||||
# Copy the response to avoid mutating the dict returned by the library
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
},
|
||||
"title": "[%key:component::here_travel_time::config::step::destination_menu::title%]"
|
||||
},
|
||||
"destination_entity_id": {
|
||||
"destination_entity": {
|
||||
"data": {
|
||||
"destination_entity_id": "Destination using an entity"
|
||||
},
|
||||
@@ -34,7 +34,7 @@
|
||||
},
|
||||
"title": "[%key:component::here_travel_time::config::step::origin_menu::title%]"
|
||||
},
|
||||
"origin_entity_id": {
|
||||
"origin_entity": {
|
||||
"data": {
|
||||
"origin_entity_id": "Origin using an entity"
|
||||
},
|
||||
|
||||
@@ -12,8 +12,7 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"username": "[%key:common::config_flow::data::username%]"
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Hi-Link HLK-SW-16 device."
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
"step": {
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"username": "[%key:common::config_flow::data::username%]"
|
||||
},
|
||||
"description": "The Honeywell integration needs to re-authenticate your account",
|
||||
"title": "[%key:common::config_flow::title::reauth%]"
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"id": "Hue bridge"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Hue bridge."
|
||||
|
||||
@@ -81,6 +81,7 @@ class ImmichDataUpdateCoordinator(DataUpdateCoordinator[ImmichData]):
|
||||
async def _async_setup(self) -> None:
|
||||
"""Handle setup of the coordinator."""
|
||||
try:
|
||||
await self.api.async_setup()
|
||||
user_info = await self.api.users.async_get_my_user()
|
||||
except ImmichUnauthorizedError as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
@@ -119,7 +120,7 @@ class ImmichDataUpdateCoordinator(DataUpdateCoordinator[ImmichData]):
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_error",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
translation_placeholders={"error": str(err)},
|
||||
) from err
|
||||
|
||||
return ImmichData(
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioimmich"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioimmich==0.14.1"]
|
||||
"requirements": ["aioimmich==0.15.0"]
|
||||
}
|
||||
|
||||
@@ -225,10 +225,9 @@ class ImmichMediaSource(MediaSource):
|
||||
entry.title,
|
||||
)
|
||||
try:
|
||||
album_info = await immich_api.albums.async_get_album_info(
|
||||
identifier.collection_id
|
||||
assets = await immich_api.search.async_get_all_by_album_ids(
|
||||
[identifier.collection_id]
|
||||
)
|
||||
assets = album_info.assets
|
||||
except ImmichForbiddenError as err:
|
||||
raise BrowseError(
|
||||
translation_domain=DOMAIN,
|
||||
|
||||
@@ -53,7 +53,7 @@ async def _async_upload_file(service_call: ServiceCall) -> None:
|
||||
|
||||
if target_album := service_call.data.get(CONF_ALBUM_ID):
|
||||
try:
|
||||
await coordinator.api.albums.async_get_album_info(target_album, True)
|
||||
await coordinator.api.albums.async_get_album_info(target_album)
|
||||
except ImmichError as ex:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
|
||||
@@ -423,7 +423,7 @@ def get_influx_connection( # noqa: C901
|
||||
if CONF_HOST in conf:
|
||||
kwargs[CONF_HOST] = conf[CONF_HOST]
|
||||
|
||||
if (path := conf.get(CONF_PATH)) is not None:
|
||||
if (path := conf.get(CONF_PATH)) is not None and path != "/":
|
||||
kwargs[CONF_PATH] = path
|
||||
|
||||
if (port := conf.get(CONF_PORT)) is not None:
|
||||
|
||||
@@ -31,7 +31,8 @@
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"protocol": "Protocol"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "Hostname or IP address of your Iskra device."
|
||||
|
||||
@@ -5,6 +5,10 @@
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"location": "[%key:common::config_flow::data::location%]",
|
||||
"name": "[%key:common::config_flow::data::name%]"
|
||||
},
|
||||
"description": "Do you want to set up Islamic Prayer Times?",
|
||||
"title": "Set up Islamic Prayer Times"
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["pyituran==0.1.5"]
|
||||
"requirements": ["pyituran==0.1.6"]
|
||||
}
|
||||
|
||||
@@ -10,6 +10,11 @@
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"step": {
|
||||
"location": {
|
||||
"data": {
|
||||
"location": "[%key:common::config_flow::data::location%]"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
|
||||
@@ -121,7 +121,7 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
native_step=PRECISION_TENTHS,
|
||||
native_min_value=0,
|
||||
native_max_value=10,
|
||||
native_max_value=30,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
set_value_fn=(
|
||||
lambda machine, value: machine.set_pre_extraction_times(
|
||||
@@ -163,7 +163,7 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
native_step=PRECISION_TENTHS,
|
||||
native_min_value=0,
|
||||
native_max_value=10,
|
||||
native_max_value=30,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
set_value_fn=(
|
||||
lambda machine, value: machine.set_pre_extraction_times(
|
||||
@@ -207,7 +207,7 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
native_step=PRECISION_TENTHS,
|
||||
native_min_value=0,
|
||||
native_max_value=10,
|
||||
native_max_value=30,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
set_value_fn=(
|
||||
lambda machine, value: machine.set_pre_extraction_times(
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
"invalid_ics_file": "There was a problem reading the calendar information. See the error log for additional details."
|
||||
},
|
||||
"step": {
|
||||
"import": {
|
||||
"import_ics_file": {
|
||||
"data": {
|
||||
"ics_file": "ICS file"
|
||||
},
|
||||
"description": "You can import events in iCal format (.ics file)."
|
||||
},
|
||||
"user": {
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"ip_address": "[%key:common::config_flow::data::ip%]"
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,11 @@
|
||||
"bluetooth_confirm": {
|
||||
"description": "Do you want to add the Melnor Bluetooth valve `{name}` to Home Assistant?",
|
||||
"title": "Discovered Melnor Bluetooth valve"
|
||||
},
|
||||
"pick_device": {
|
||||
"data": {
|
||||
"address": "[%key:common::config_flow::data::device%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"code": "Station code"
|
||||
"station_code": "Station code"
|
||||
},
|
||||
"data_description": {
|
||||
"code": "Looks like ESCAT4300000043206B"
|
||||
"station_code": "Looks like ESCAT4300000043206B"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -508,19 +508,20 @@ class DishWasherProgramId(MieleEnum, missing_to_none=True):
|
||||
solar_save = 9, 34
|
||||
gentle = 10, 35, 210
|
||||
extra_quiet = 11, 36, 207
|
||||
hygiene = 12, 37
|
||||
quick_power_wash = 13, 38
|
||||
hygiene = 12, 37, 206
|
||||
quick_power_wash = 13, 38, 216
|
||||
pasta_paela = 14
|
||||
tall_items = 17, 42
|
||||
glasses_warm = 19
|
||||
quick_intense = 21
|
||||
normal = 23, 30
|
||||
normal = 23, 30, 217
|
||||
pre_wash = 24
|
||||
pot_rests_and_filters = 25
|
||||
power_wash = 44, 204
|
||||
comfort_wash = 203
|
||||
comfort_wash_plus = 209
|
||||
rinse_salt = 215
|
||||
rinse_and_hold = 219
|
||||
|
||||
|
||||
class TumbleDryerProgramId(MieleEnum, missing_to_none=True):
|
||||
|
||||
@@ -135,6 +135,8 @@ class MieleFan(MieleEntity, FanEntity):
|
||||
_LOGGER.debug("Calc ventilation_step: %s", ventilation_step)
|
||||
if ventilation_step == 0:
|
||||
await self.async_turn_off()
|
||||
elif ventilation_step == self.device.state_ventilation_step:
|
||||
return
|
||||
else:
|
||||
try:
|
||||
await self.api.send_action(
|
||||
|
||||
@@ -791,6 +791,7 @@
|
||||
"rice_pudding_rapid_steam_cooking": "Rice pudding (rapid steam cooking)",
|
||||
"rice_pudding_steam_cooking": "Rice pudding (steam cooking)",
|
||||
"rinse": "Rinse",
|
||||
"rinse_and_hold": "Rinse and hold",
|
||||
"rinse_out_lint": "Rinse out lint",
|
||||
"rinse_salt": "Rinse salt",
|
||||
"risotto": "Risotto",
|
||||
|
||||
@@ -14,8 +14,7 @@
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
"device": "[%key:common::config_flow::data::device%]"
|
||||
},
|
||||
"description": "This is an integration for landline calls using a CX93001 voice modem. This can retrieve caller ID information with an option to reject an incoming call."
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
},
|
||||
"step": {
|
||||
"confirm": {
|
||||
"data": {
|
||||
"blind_type": "Blind type"
|
||||
},
|
||||
"description": "What kind of blind is {display_name}?"
|
||||
},
|
||||
"user": {
|
||||
|
||||
@@ -4179,7 +4179,6 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
CONF_PROTOCOL: DEFAULT_PROTOCOL,
|
||||
CONF_USERNAME: addon_discovery_config.get(CONF_USERNAME),
|
||||
CONF_PASSWORD: addon_discovery_config.get(CONF_PASSWORD),
|
||||
CONF_DISCOVERY: DEFAULT_DISCOVERY,
|
||||
}
|
||||
except AddonError:
|
||||
# We do not have discovery information yet
|
||||
@@ -4420,7 +4419,6 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
CONF_PROTOCOL: DEFAULT_PROTOCOL,
|
||||
CONF_USERNAME: data.get(CONF_USERNAME),
|
||||
CONF_PASSWORD: data.get(CONF_PASSWORD),
|
||||
CONF_DISCOVERY: DEFAULT_DISCOVERY,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["opower"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["opower==0.18.4"]
|
||||
"requirements": ["opower==0.18.5"]
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
"step": {
|
||||
"confirm": {
|
||||
"data": {
|
||||
"code": "Verification code (OTP)"
|
||||
"code": "Verification code (OTP)",
|
||||
"qr_code": "QR code"
|
||||
},
|
||||
"data_description": {
|
||||
"code": "The six-digit code currently displayed in your authentication app."
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"update_interval": "Update interval (minutes)"
|
||||
"scan_interval": "Update interval (minutes)"
|
||||
},
|
||||
"description": "Set the update interval (minutes)",
|
||||
"title": "Options for Plaato"
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyrainbird"],
|
||||
"requirements": ["pyrainbird==6.3.0"]
|
||||
"requirements": ["pyrainbird==6.3.1"]
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ ENTITY_DESCRIPTIONS = (
|
||||
key="fill",
|
||||
translation_key="fill",
|
||||
value_fn=lambda diffuser: diffuser.fill,
|
||||
has_fn=lambda diffuser: "fillc" in diffuser.hub_data.get("sensors", {}),
|
||||
),
|
||||
RitualsSensorEntityDescription(
|
||||
key="perfume",
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/scrape",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["beautifulsoup4==4.13.3", "lxml==6.0.1"]
|
||||
"requirements": ["beautifulsoup4==4.13.3", "lxml==6.1.1"]
|
||||
}
|
||||
|
||||
@@ -58,6 +58,38 @@
|
||||
"user": "Add sensor"
|
||||
},
|
||||
"step": {
|
||||
"reconfigure": {
|
||||
"data": {
|
||||
"index": "[%key:component::scrape::config_subentries::entity::step::user::data::index%]",
|
||||
"select": "[%key:component::scrape::config_subentries::entity::step::user::data::select%]"
|
||||
},
|
||||
"data_description": {
|
||||
"index": "[%key:component::scrape::config_subentries::entity::step::user::data_description::index%]",
|
||||
"select": "[%key:component::scrape::config_subentries::entity::step::user::data_description::select%]"
|
||||
},
|
||||
"sections": {
|
||||
"advanced": {
|
||||
"data": {
|
||||
"attribute": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data::attribute%]",
|
||||
"availability": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data::availability%]",
|
||||
"device_class": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data::device_class%]",
|
||||
"state_class": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data::state_class%]",
|
||||
"unit_of_measurement": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data::unit_of_measurement%]",
|
||||
"value_template": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data::value_template%]"
|
||||
},
|
||||
"data_description": {
|
||||
"attribute": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data_description::attribute%]",
|
||||
"availability": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data_description::availability%]",
|
||||
"device_class": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data_description::device_class%]",
|
||||
"state_class": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data_description::state_class%]",
|
||||
"unit_of_measurement": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data_description::unit_of_measurement%]",
|
||||
"value_template": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data_description::value_template%]"
|
||||
},
|
||||
"description": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::description%]",
|
||||
"name": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::name%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"index": "Index",
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"step": {
|
||||
"additional_account": {
|
||||
"add_account": {
|
||||
"data": {
|
||||
"account": "[%key:component::sia::config::step::user::data::account%]",
|
||||
"additional_account": "[%key:component::sia::config::step::user::data::additional_account%]",
|
||||
|
||||
@@ -247,7 +247,7 @@ def _async_register_base_station(
|
||||
config_entry_id=entry.entry_id,
|
||||
identifiers={(DOMAIN, str(system.system_id))},
|
||||
manufacturer="SimpliSafe",
|
||||
model=system.version,
|
||||
model=str(system.version),
|
||||
name=system.address,
|
||||
)
|
||||
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"address": "[%key:common::config_flow::data::device%]"
|
||||
"address": "[%key:common::config_flow::data::device%]",
|
||||
"name": "[%key:common::config_flow::data::name%]"
|
||||
},
|
||||
"description": "[%key:component::bluetooth::config::step::user::description%]"
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ from .const import (
|
||||
MODELS_TV_ONLY,
|
||||
PLAYABLE_MEDIA_TYPES,
|
||||
SONOS_CREATE_MEDIA_PLAYER,
|
||||
SONOS_FAVORITES_UPDATED,
|
||||
SONOS_MEDIA_UPDATED,
|
||||
SONOS_STATE_PLAYING,
|
||||
SONOS_STATE_TRANSITIONING,
|
||||
@@ -131,7 +132,6 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
|
||||
| MediaPlayerEntityFeature.REPEAT_SET
|
||||
| MediaPlayerEntityFeature.SEARCH_MEDIA
|
||||
| MediaPlayerEntityFeature.SEEK
|
||||
| MediaPlayerEntityFeature.SELECT_SOURCE
|
||||
| MediaPlayerEntityFeature.SHUFFLE_SET
|
||||
| MediaPlayerEntityFeature.STOP
|
||||
| MediaPlayerEntityFeature.VOLUME_MUTE
|
||||
@@ -145,6 +145,14 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
|
||||
super().__init__(speaker, config_entry)
|
||||
self._attr_unique_id = self.soco.uid
|
||||
|
||||
@property
|
||||
def supported_features(self) -> MediaPlayerEntityFeature:
|
||||
"""Flag media player features that are supported."""
|
||||
features = self._attr_supported_features
|
||||
if self.source_list:
|
||||
features |= MediaPlayerEntityFeature.SELECT_SOURCE
|
||||
return features
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Handle common setup when added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
@@ -155,6 +163,13 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
|
||||
self.async_write_media_state,
|
||||
)
|
||||
)
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
f"{SONOS_FAVORITES_UPDATED}-{self.speaker.household_id}",
|
||||
self.async_write_ha_state,
|
||||
)
|
||||
)
|
||||
|
||||
@callback
|
||||
def async_write_media_state(self, uid: str) -> None:
|
||||
@@ -394,14 +409,18 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
|
||||
@property
|
||||
def source_list(self) -> list[str]:
|
||||
"""List of available input sources."""
|
||||
sources: list[str] = []
|
||||
model = self.coordinator.model_name.split()[-1].upper()
|
||||
if model in MODELS_LINEIN_ONLY:
|
||||
return [SOURCE_LINEIN]
|
||||
if model in MODELS_TV_ONLY:
|
||||
return [SOURCE_TV]
|
||||
if model in MODELS_LINEIN_AND_TV:
|
||||
return [SOURCE_LINEIN, SOURCE_TV]
|
||||
return []
|
||||
sources = [SOURCE_LINEIN]
|
||||
elif model in MODELS_TV_ONLY:
|
||||
sources = [SOURCE_TV]
|
||||
elif model in MODELS_LINEIN_AND_TV:
|
||||
sources = [SOURCE_LINEIN, SOURCE_TV]
|
||||
sources.extend(
|
||||
fav.title for fav in self.speaker.favorites if fav.title not in sources
|
||||
)
|
||||
return sources
|
||||
|
||||
@soco_error(UPNP_ERRORS_TO_IGNORE)
|
||||
def media_play(self) -> None:
|
||||
|
||||
@@ -160,7 +160,7 @@ async def async_setup_entry(
|
||||
model=model,
|
||||
manufacturer=manufacturer,
|
||||
model_id=model_id,
|
||||
hw_version=player.firmware,
|
||||
hw_version=str(player.firmware) if player.firmware is not None else None,
|
||||
sw_version=sw_version,
|
||||
via_device=(DOMAIN, coordinator.server_uuid),
|
||||
)
|
||||
|
||||
@@ -29,6 +29,7 @@ VEHICLE_STATUS = "vehicle_status"
|
||||
API_GEN_1 = "g1"
|
||||
API_GEN_2 = "g2"
|
||||
API_GEN_3 = "g3"
|
||||
API_GEN_4 = "g4"
|
||||
MANUFACTURER = "Subaru"
|
||||
|
||||
PLATFORMS = [
|
||||
|
||||
@@ -24,6 +24,7 @@ from . import get_device_info
|
||||
from .const import (
|
||||
API_GEN_2,
|
||||
API_GEN_3,
|
||||
API_GEN_4,
|
||||
VEHICLE_API_GEN,
|
||||
VEHICLE_HAS_EV,
|
||||
VEHICLE_STATUS,
|
||||
@@ -153,10 +154,10 @@ def create_vehicle_sensors(
|
||||
sensor_descriptions_to_add = []
|
||||
sensor_descriptions_to_add.extend(SAFETY_SENSORS)
|
||||
|
||||
if vehicle_info[VEHICLE_API_GEN] in [API_GEN_2, API_GEN_3]:
|
||||
if vehicle_info[VEHICLE_API_GEN] in [API_GEN_2, API_GEN_3, API_GEN_4]:
|
||||
sensor_descriptions_to_add.extend(API_GEN_2_SENSORS)
|
||||
|
||||
if vehicle_info[VEHICLE_API_GEN] == API_GEN_3:
|
||||
if vehicle_info[VEHICLE_API_GEN] in [API_GEN_3, API_GEN_4]:
|
||||
sensor_descriptions_to_add.extend(API_GEN_3_SENSORS)
|
||||
|
||||
if vehicle_info[VEHICLE_HAS_EV]:
|
||||
|
||||
@@ -36,8 +36,11 @@ class SwitchBotCloudEntity(CoordinatorEntity[SwitchBotCoordinator]):
|
||||
self._api = api
|
||||
self._attr_unique_id = device.device_id
|
||||
_sw_version = None
|
||||
if self.coordinator.data is not None:
|
||||
_sw_version = self.coordinator.data.get("version")
|
||||
if (
|
||||
self.coordinator.data is not None
|
||||
and (_version := self.coordinator.data.get("version")) is not None
|
||||
):
|
||||
_sw_version = str(_version)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, device.device_id)},
|
||||
name=device.device_name,
|
||||
|
||||
@@ -61,6 +61,7 @@ def setup_platform(
|
||||
if http_error.response.status_code == requests.codes.unauthorized:
|
||||
_LOGGER.error("Invalid credentials")
|
||||
return
|
||||
raise
|
||||
|
||||
all_sensors = []
|
||||
for device in devices:
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/thread",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["python-otbr-api==2.10.0", "pyroute2==0.7.5"],
|
||||
"requirements": ["python-otbr-api==2.10.0", "pyroute2==0.9.6"],
|
||||
"single_config_entry": true,
|
||||
"zeroconf": ["_meshcop._udp.local."]
|
||||
}
|
||||
|
||||
@@ -12,6 +12,12 @@
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"step": {
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"email": "[%key:common::config_flow::data::email%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"email": "[%key:common::config_flow::data::email%]",
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
"description": "The Tuya integration now uses an improved login method. To reauthenticate with your Smart Life or Tuya Smart account, you need to enter your user code.\n\nYou can find this code in the Smart Life app or Tuya Smart app in **Settings** > **Account and Security** screen, and enter the code shown on the **User Code** field. The user code is case-sensitive, please be sure to enter it exactly as shown in the app."
|
||||
},
|
||||
"scan": {
|
||||
"data": {
|
||||
"QR": "QR code"
|
||||
},
|
||||
"description": "Use the Smart Life app or Tuya Smart app to scan the following QR code to complete the login.\n\nContinue to the next step once you have completed this step in the app."
|
||||
},
|
||||
"user": {
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
import logging
|
||||
|
||||
from aiowebdav2.client import Client
|
||||
from aiowebdav2.exceptions import UnauthorizedError
|
||||
from aiowebdav2.exceptions import (
|
||||
ConnectionExceptionError,
|
||||
NoConnectionError,
|
||||
UnauthorizedError,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME, CONF_VERIFY_SSL
|
||||
@@ -35,6 +39,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: WebDavConfigEntry) -> bo
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_username_password",
|
||||
) from err
|
||||
except (ConnectionExceptionError, NoConnectionError, TimeoutError) as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
) from err
|
||||
|
||||
# Check if we can connect to the WebDAV server
|
||||
# and access the root directory
|
||||
|
||||
@@ -5,6 +5,7 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
from weheat.abstractions.user import async_get_user_id_from_token
|
||||
from weheat.exceptions import ApiException
|
||||
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
|
||||
@@ -33,12 +34,17 @@ class OAuth2FlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
|
||||
async def async_oauth_create_entry(self, data: dict) -> ConfigFlowResult:
|
||||
"""Override create entry to find heat pumps."""
|
||||
# get the user id and use that as unique id for this entry
|
||||
user_id = await async_get_user_id_from_token(
|
||||
API_URL,
|
||||
data[CONF_TOKEN][CONF_ACCESS_TOKEN],
|
||||
async_get_clientsession(self.hass),
|
||||
)
|
||||
try:
|
||||
user_id = await async_get_user_id_from_token(
|
||||
API_URL,
|
||||
data[CONF_TOKEN][CONF_ACCESS_TOKEN],
|
||||
async_get_clientsession(self.hass),
|
||||
)
|
||||
except ApiException as err:
|
||||
self.logger.error("Failed to get user ID from Weheat API: %s", err)
|
||||
return self.async_abort(reason="oauth_failed")
|
||||
if user_id is None:
|
||||
return self.async_abort(reason="oauth_failed")
|
||||
await self.async_set_unique_id(user_id)
|
||||
if self.source != SOURCE_REAUTH:
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
@@ -177,6 +177,6 @@ class XiaomiGatewayDevice(CoordinatorEntity[GatewayDeviceCoordinator], Entity):
|
||||
manufacturer="Xiaomi",
|
||||
name=self._sub_device.name,
|
||||
model=self._sub_device.model,
|
||||
sw_version=self._sub_device.firmware_version,
|
||||
sw_version=str(self._sub_device.firmware_version),
|
||||
hw_version=self._sub_device.zigbee_model,
|
||||
)
|
||||
|
||||
@@ -432,15 +432,6 @@ async def async_set_credential(
|
||||
translation_key="no_available_credential_slots",
|
||||
translation_placeholders={"credential_type": cred_type_str},
|
||||
)
|
||||
elif not 1 <= credential_slot <= type_cap.number_of_credential_slots:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="credential_slot_out_of_range",
|
||||
translation_placeholders={
|
||||
"credential_type": cred_type_str,
|
||||
"max_slot": str(type_cap.number_of_credential_slots),
|
||||
},
|
||||
)
|
||||
|
||||
status = await node.access_control.set_credential(
|
||||
user_id, credential_type, credential_slot, credential_data
|
||||
|
||||
@@ -322,9 +322,6 @@
|
||||
"credential_rejected_wrong_uuid": {
|
||||
"message": "The device rejected the credential because the user unique identifier does not match."
|
||||
},
|
||||
"credential_slot_out_of_range": {
|
||||
"message": "Credential slot for {credential_type} must be between 1 and {max_slot}."
|
||||
},
|
||||
"credential_type_not_supported": {
|
||||
"message": "Credential type {credential_type} is not supported on this device"
|
||||
},
|
||||
|
||||
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2026
|
||||
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, 14, 2)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Automatically generated by gen_requirements_all.py, do not edit
|
||||
|
||||
aiodhcpwatcher==1.2.7
|
||||
aiodiscover==3.2.4
|
||||
aiodiscover==3.3.2
|
||||
aiodns==4.0.4
|
||||
aiogithubapi==26.0.0
|
||||
aiohttp-asyncmdnsresolver==0.2.0
|
||||
@@ -25,7 +25,7 @@ bleak==3.0.2
|
||||
bluetooth-adapters==2.3.0
|
||||
bluetooth-auto-recovery==1.6.4
|
||||
bluetooth-data-tools==1.29.18
|
||||
cached-ipaddress==1.1.1
|
||||
cached-ipaddress==1.1.2
|
||||
certifi>=2021.5.30
|
||||
ciso8601==2.3.3
|
||||
cronsim==2.7
|
||||
@@ -39,7 +39,7 @@ habluetooth==6.8.1
|
||||
hass-nabucasa==2.2.0
|
||||
hassil==3.7.0
|
||||
home-assistant-bluetooth==2.0.0
|
||||
home-assistant-frontend==20260527.6
|
||||
home-assistant-frontend==20260527.7
|
||||
home-assistant-intents==2026.6.1
|
||||
httpx==0.28.1
|
||||
ifaddr==0.2.0
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ To update, run python3 -m script.hassfest
|
||||
|
||||
from typing import Final
|
||||
|
||||
FRONTEND_VERSION: Final[str] = "20260527.6"
|
||||
FRONTEND_VERSION: Final[str] = "20260527.7"
|
||||
|
||||
MDI_ICONS: Final[set[str]] = {
|
||||
"ab-testing",
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2026.6.3"
|
||||
version = "2026.6.4"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
|
||||
Generated
+12
-12
@@ -190,7 +190,7 @@ aioairzone-cloud==0.7.2
|
||||
aioairzone==1.0.5
|
||||
|
||||
# homeassistant.components.alexa_devices
|
||||
aioamazondevices==14.0.3
|
||||
aioamazondevices==14.1.3
|
||||
|
||||
# homeassistant.components.ambient_network
|
||||
# homeassistant.components.ambient_station
|
||||
@@ -230,13 +230,13 @@ aiobotocore==2.21.1
|
||||
aiocentriconnect==0.2.3
|
||||
|
||||
# homeassistant.components.comelit
|
||||
aiocomelit==2.0.3
|
||||
aiocomelit==2.0.7
|
||||
|
||||
# homeassistant.components.dhcp
|
||||
aiodhcpwatcher==1.2.7
|
||||
|
||||
# homeassistant.components.dhcp
|
||||
aiodiscover==3.2.4
|
||||
aiodiscover==3.3.2
|
||||
|
||||
# homeassistant.components.dnsip
|
||||
aiodns==4.0.4
|
||||
@@ -300,7 +300,7 @@ aiohue==4.8.1
|
||||
aioimaplib==2.0.1
|
||||
|
||||
# homeassistant.components.immich
|
||||
aioimmich==0.14.1
|
||||
aioimmich==0.15.0
|
||||
|
||||
# homeassistant.components.apache_kafka
|
||||
aiokafka==0.10.0
|
||||
@@ -730,7 +730,7 @@ btsmarthub-devicelist==0.2.3
|
||||
buienradar==1.0.6
|
||||
|
||||
# homeassistant.components.dhcp
|
||||
cached-ipaddress==1.1.1
|
||||
cached-ipaddress==1.1.2
|
||||
|
||||
# homeassistant.components.caldav
|
||||
caldav==2.1.0
|
||||
@@ -853,7 +853,7 @@ dremel3dpy==2.1.1
|
||||
dropmqttapi==1.0.3
|
||||
|
||||
# homeassistant.components.dsmr
|
||||
dsmr-parser==1.7.0
|
||||
dsmr-parser==1.9.0
|
||||
|
||||
# homeassistant.components.dwd_weather_warnings
|
||||
dwdwfsapi==1.0.7
|
||||
@@ -1266,7 +1266,7 @@ hole==0.9.0
|
||||
holidays==0.98
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20260527.6
|
||||
home-assistant-frontend==20260527.7
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2026.6.1
|
||||
@@ -1513,7 +1513,7 @@ lupupy==0.3.2
|
||||
lw12==0.9.2
|
||||
|
||||
# homeassistant.components.scrape
|
||||
lxml==6.0.1
|
||||
lxml==6.1.1
|
||||
|
||||
# homeassistant.components.matrix
|
||||
matrix-nio==0.25.2
|
||||
@@ -1776,7 +1776,7 @@ openwrt-luci-rpc==1.1.17
|
||||
openwrt-ubus-rpc==0.0.2
|
||||
|
||||
# homeassistant.components.opower
|
||||
opower==0.18.4
|
||||
opower==0.18.5
|
||||
|
||||
# homeassistant.components.oralb
|
||||
oralb-ble==1.1.0
|
||||
@@ -2253,7 +2253,7 @@ pyisy==3.6.1
|
||||
pyitachip2ir==0.0.7
|
||||
|
||||
# homeassistant.components.ituran
|
||||
pyituran==0.1.5
|
||||
pyituran==0.1.6
|
||||
|
||||
# homeassistant.components.jvc_projector
|
||||
pyjvcprojector==2.0.6
|
||||
@@ -2474,7 +2474,7 @@ pyqwikswitch==0.93
|
||||
pyrail==0.4.1
|
||||
|
||||
# homeassistant.components.rainbird
|
||||
pyrainbird==6.3.0
|
||||
pyrainbird==6.3.1
|
||||
|
||||
# homeassistant.components.playstation_network
|
||||
pyrate-limiter==4.1.0
|
||||
@@ -2492,7 +2492,7 @@ pyrisco==0.7.0
|
||||
pyrituals==0.0.7
|
||||
|
||||
# homeassistant.components.thread
|
||||
pyroute2==0.7.5
|
||||
pyroute2==0.9.6
|
||||
|
||||
# homeassistant.components.rympro
|
||||
pyrympro==0.0.9
|
||||
|
||||
@@ -77,6 +77,7 @@ TEST_DEVICE_1 = AmazonDevice(
|
||||
),
|
||||
},
|
||||
media_player_supported=True,
|
||||
communication_settings={},
|
||||
)
|
||||
|
||||
TEST_DEVICE_2_SN = "echo_test_2_serial_number"
|
||||
@@ -109,6 +110,7 @@ TEST_DEVICE_2 = AmazonDevice(
|
||||
notifications_supported=False,
|
||||
notifications={},
|
||||
media_player_supported=False,
|
||||
communication_settings={},
|
||||
)
|
||||
|
||||
TEST_VOCAL_RECORD_INITIAL = AmazonVocalRecord(
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
'AUDIO_PLAYER',
|
||||
'MICROPHONE',
|
||||
]),
|
||||
'communication_settings': dict({
|
||||
}),
|
||||
'device_cluster_members': dict({
|
||||
'echo_test_serial_number': 'echo_test_device_id',
|
||||
}),
|
||||
@@ -79,6 +81,8 @@
|
||||
'AUDIO_PLAYER',
|
||||
'MICROPHONE',
|
||||
]),
|
||||
'communication_settings': dict({
|
||||
}),
|
||||
'device_cluster_members': dict({
|
||||
'echo_test_serial_number': 'echo_test_device_id',
|
||||
}),
|
||||
@@ -150,6 +154,8 @@
|
||||
'AUDIO_PLAYER',
|
||||
'MICROPHONE',
|
||||
]),
|
||||
'communication_settings': dict({
|
||||
}),
|
||||
'device_cluster_members': dict({
|
||||
'echo_test_serial_number': 'echo_test_device_id',
|
||||
}),
|
||||
|
||||
@@ -216,34 +216,15 @@ async def test_single_site(hass: HomeAssistant, single_site_api: Mock) -> None:
|
||||
async def test_single_closed_site_no_closed_date(
|
||||
hass: HomeAssistant, single_site_closed_no_close_date_api: Mock
|
||||
) -> None:
|
||||
"""Test single closed site with no closed date."""
|
||||
initial_result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert initial_result.get("type") is FlowResultType.FORM
|
||||
assert initial_result.get("step_id") == "user"
|
||||
|
||||
# Test filling in API key
|
||||
"""Test single closed site with no closed date is filtered out."""
|
||||
enter_api_key_result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
data={CONF_API_TOKEN: API_KEY},
|
||||
)
|
||||
assert enter_api_key_result.get("type") is FlowResultType.FORM
|
||||
assert enter_api_key_result.get("step_id") == "site"
|
||||
|
||||
select_site_result = await hass.config_entries.flow.async_configure(
|
||||
enter_api_key_result["flow_id"],
|
||||
{CONF_SITE_ID: "01FG0AGP818PXK0DWHXJRRT2DH", CONF_SITE_NAME: "Home"},
|
||||
)
|
||||
|
||||
# Show available sites
|
||||
assert select_site_result.get("type") is FlowResultType.CREATE_ENTRY
|
||||
assert select_site_result.get("title") == "Home"
|
||||
data = select_site_result.get("data")
|
||||
assert data
|
||||
assert data[CONF_API_TOKEN] == API_KEY
|
||||
assert data[CONF_SITE_ID] == "01FG0AGP818PXK0DWHXJRRT2DH"
|
||||
assert enter_api_key_result.get("step_id") == "user"
|
||||
assert enter_api_key_result.get("errors") == {"api_token": "no_site"}
|
||||
|
||||
|
||||
async def test_single_site_rejoin(
|
||||
@@ -333,13 +314,9 @@ async def test_unknown_error(hass: HomeAssistant, api_error: Mock) -> None:
|
||||
assert result.get("errors") == {"api_token": "unknown_error"}
|
||||
|
||||
|
||||
async def test_site_deduplication(single_site_rejoin_api: Mock) -> None:
|
||||
"""Test site deduplication."""
|
||||
async def test_site_filtering(single_site_rejoin_api: Mock) -> None:
|
||||
"""Test that closed sites are filtered out and remaining sites are deduplicated."""
|
||||
filtered = filter_sites(single_site_rejoin_api.get_sites())
|
||||
assert len(filtered) == 2
|
||||
assert (
|
||||
next(s for s in filtered if s.nmi == "11111111111").status == SiteStatus.ACTIVE
|
||||
)
|
||||
assert (
|
||||
next(s for s in filtered if s.nmi == "11111111112").status == SiteStatus.CLOSED
|
||||
)
|
||||
assert len(filtered) == 1
|
||||
assert filtered[0].nmi == "11111111111"
|
||||
assert filtered[0].status == SiteStatus.ACTIVE
|
||||
|
||||
@@ -2396,3 +2396,47 @@ async def test_user_setup_replaces_ignored_device(hass: HomeAssistant) -> None:
|
||||
|
||||
assert result3["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result3["result"].unique_id == dr.format_mac(MOCK_MAC)
|
||||
|
||||
|
||||
async def test_reconfigure_no_unique_id_with_discovery(hass: HomeAssistant) -> None:
|
||||
"""Test reconfigure succeeds when entry has no unique_id and device is discovered."""
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
CONF_HOST: f"elks://{MOCK_IP_ADDRESS}",
|
||||
CONF_USERNAME: "test-username",
|
||||
CONF_PASSWORD: "test-password",
|
||||
CONF_PREFIX: "",
|
||||
},
|
||||
unique_id=None,
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
result = await config_entry.start_reconfigure_flow(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "reconfigure"
|
||||
|
||||
mocked_elk = mock_elk(invalid_auth=False, sync_complete=True)
|
||||
|
||||
with (
|
||||
_patch_discovery(),
|
||||
_patch_elk(mocked_elk),
|
||||
patch(
|
||||
"homeassistant.components.elkm1.async_setup_entry",
|
||||
return_value=True,
|
||||
),
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_PROTOCOL: "non-secure",
|
||||
CONF_ADDRESS: MOCK_IP_ADDRESS,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] is FlowResultType.ABORT
|
||||
assert result2["reason"] == "reconfigure_successful"
|
||||
assert config_entry.unique_id == dr.format_mac(MOCK_MAC)
|
||||
|
||||
@@ -138,10 +138,12 @@ def mock_growatt_v1_api():
|
||||
}
|
||||
|
||||
# Called by total coordinator during refresh
|
||||
# Note: V1 API returns current_power in kW; the coordinator
|
||||
# converts it to W when mapping to invTodayPpv.
|
||||
mock_v1_api.plant_energy_overview.return_value = {
|
||||
"today_energy": 12.5,
|
||||
"total_energy": 1250.0,
|
||||
"current_power": 2500,
|
||||
"current_power": 2.5,
|
||||
}
|
||||
|
||||
# Called by switch/number entities during turn_on/turn_off/set_value
|
||||
|
||||
@@ -88,8 +88,8 @@
|
||||
}),
|
||||
]),
|
||||
'total_coordinator': dict({
|
||||
'current_power': 2500,
|
||||
'invTodayPpv': 2500,
|
||||
'current_power': 2.5,
|
||||
'invTodayPpv': 2500.0,
|
||||
'todayEnergy': 12.5,
|
||||
'today_energy': 12.5,
|
||||
'totalEnergy': 1250.0,
|
||||
|
||||
@@ -3881,7 +3881,7 @@
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '2500',
|
||||
'state': '2500.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_min_sensors_v1_api[sensor.test_plant_total_total_money_today-entry]
|
||||
@@ -15939,7 +15939,7 @@
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '2500',
|
||||
'state': '2500.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_sph_sensors_v1_api[sensor.test_plant_total_total_money_today-entry]
|
||||
|
||||
@@ -169,7 +169,7 @@ async def test_sensor_coordinator_updates(
|
||||
mock_growatt_v1_api.plant_energy_overview.return_value = {
|
||||
"today_energy": 25.0, # Changed from 12.5
|
||||
"total_energy": 1250.0,
|
||||
"current_power": 2500,
|
||||
"current_power": 2.5,
|
||||
}
|
||||
|
||||
# Trigger coordinator refresh
|
||||
@@ -284,7 +284,7 @@ async def test_midnight_bounce_suppression(
|
||||
mock_growatt_v1_api.plant_energy_overview.return_value = {
|
||||
"today_energy": 0.1,
|
||||
"total_energy": 1250.1,
|
||||
"current_power": 500,
|
||||
"current_power": 0.5,
|
||||
}
|
||||
freezer.tick(timedelta(minutes=5))
|
||||
async_fire_time_changed(hass)
|
||||
@@ -339,7 +339,7 @@ async def test_normal_reset_no_bounce(
|
||||
mock_growatt_v1_api.plant_energy_overview.return_value = {
|
||||
"today_energy": 0.1,
|
||||
"total_energy": 1250.1,
|
||||
"current_power": 500,
|
||||
"current_power": 0.5,
|
||||
}
|
||||
freezer.tick(timedelta(minutes=5))
|
||||
async_fire_time_changed(hass)
|
||||
@@ -353,7 +353,7 @@ async def test_normal_reset_no_bounce(
|
||||
mock_growatt_v1_api.plant_energy_overview.return_value = {
|
||||
"today_energy": 1.5,
|
||||
"total_energy": 1251.5,
|
||||
"current_power": 2000,
|
||||
"current_power": 2.0,
|
||||
}
|
||||
freezer.tick(timedelta(minutes=5))
|
||||
async_fire_time_changed(hass)
|
||||
@@ -452,7 +452,7 @@ async def test_midnight_bounce_repeated(
|
||||
mock_growatt_v1_api.plant_energy_overview.return_value = {
|
||||
"today_energy": 0.2,
|
||||
"total_energy": 1250.2,
|
||||
"current_power": 1000,
|
||||
"current_power": 1.0,
|
||||
}
|
||||
freezer.tick(timedelta(minutes=5))
|
||||
async_fire_time_changed(hass)
|
||||
@@ -486,7 +486,7 @@ async def test_non_total_increasing_sensor_unaffected_by_bounce_suppression(
|
||||
mock_growatt_v1_api.plant_energy_overview.return_value = {
|
||||
"today_energy": 12.5,
|
||||
"total_energy": 0,
|
||||
"current_power": 2500,
|
||||
"current_power": 2.5,
|
||||
}
|
||||
freezer.tick(timedelta(minutes=5))
|
||||
async_fire_time_changed(hass)
|
||||
@@ -500,7 +500,7 @@ async def test_non_total_increasing_sensor_unaffected_by_bounce_suppression(
|
||||
mock_growatt_v1_api.plant_energy_overview.return_value = {
|
||||
"today_energy": 12.5,
|
||||
"total_energy": 1250.0,
|
||||
"current_power": 2500,
|
||||
"current_power": 2.5,
|
||||
}
|
||||
freezer.tick(timedelta(minutes=5))
|
||||
async_fire_time_changed(hass)
|
||||
|
||||
@@ -40,8 +40,8 @@ from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util.aiohttp import MockStreamReaderChunked
|
||||
|
||||
from .const import (
|
||||
MOCK_ALBUM_WITH_ASSETS,
|
||||
MOCK_ALBUM_WITHOUT_ASSETS,
|
||||
ALBUM_DATA,
|
||||
MOCK_ALBUM_ASSETS,
|
||||
MOCK_FAVORITE_ASSETS,
|
||||
MOCK_PEOPLE_ASSETS,
|
||||
MOCK_TAGS_ASSETS,
|
||||
@@ -80,8 +80,7 @@ def mock_config_entry() -> MockConfigEntry:
|
||||
def mock_immich_albums() -> AsyncMock:
|
||||
"""Mock the Immich server."""
|
||||
mock = AsyncMock(spec=ImmichAlbums)
|
||||
mock.async_get_all_albums.return_value = [MOCK_ALBUM_WITHOUT_ASSETS]
|
||||
mock.async_get_album_info.return_value = MOCK_ALBUM_WITH_ASSETS
|
||||
mock.async_get_all_albums.return_value = [ALBUM_DATA]
|
||||
mock.async_add_assets_to_album.return_value = [
|
||||
ImmichAddAssetsToAlbumResponse.from_dict(
|
||||
{"id": "abcdef-0123456789", "success": True}
|
||||
@@ -160,6 +159,7 @@ def mock_immich_search() -> AsyncMock:
|
||||
"""Mock the Immich server."""
|
||||
mock = AsyncMock(spec=ImmichSearch)
|
||||
mock.async_get_all_favorites.return_value = MOCK_FAVORITE_ASSETS
|
||||
mock.async_get_all_by_album_ids.return_value = MOCK_ALBUM_ASSETS
|
||||
mock.async_get_all_by_person_ids.return_value = MOCK_PEOPLE_ASSETS
|
||||
mock.async_get_all_by_tag_ids.return_value = MOCK_TAGS_ASSETS
|
||||
return mock
|
||||
|
||||
@@ -26,103 +26,101 @@ MOCK_CONFIG_ENTRY_DATA = {
|
||||
CONF_VERIFY_SSL: False,
|
||||
}
|
||||
|
||||
ALBUM_DATA = {
|
||||
"id": "721e1a4b-aa12-441e-8d3b-5ac7ab283bb6",
|
||||
"albumName": "My Album",
|
||||
"albumThumbnailAssetId": "0d03a7ad-ddc7-45a6-adee-68d322a6d2f5",
|
||||
"albumUsers": [],
|
||||
"assetCount": 1,
|
||||
"assets": [],
|
||||
"createdAt": "2025-05-11T10:13:22.799Z",
|
||||
"hasSharedLink": False,
|
||||
"isActivityEnabled": False,
|
||||
"ownerId": "e7ef5713-9dab-4bd4-b899-715b0ca4379e",
|
||||
"owner": {
|
||||
"id": "e7ef5713-9dab-4bd4-b899-715b0ca4379e",
|
||||
"email": "admin@immich.local",
|
||||
"name": "admin",
|
||||
"profileImagePath": "",
|
||||
"avatarColor": "primary",
|
||||
"profileChangedAt": "2025-05-11T10:07:46.866Z",
|
||||
},
|
||||
"shared": False,
|
||||
"updatedAt": "2025-05-17T11:26:03.696Z",
|
||||
}
|
||||
|
||||
MOCK_ALBUM_WITHOUT_ASSETS = ImmichAlbum.from_dict(ALBUM_DATA)
|
||||
|
||||
MOCK_ALBUM_WITH_ASSETS = ImmichAlbum.from_dict(
|
||||
ALBUM_DATA = ImmichAlbum.from_dict(
|
||||
{
|
||||
**ALBUM_DATA,
|
||||
"assets": [
|
||||
{
|
||||
"id": "2e94c203-50aa-4ad2-8e29-56dd74e0eff4",
|
||||
"deviceAssetId": "web-filename.jpg-1675185639000",
|
||||
"ownerId": "e7ef5713-9dab-4bd4-b899-715b0ca4379e",
|
||||
"deviceId": "WEB",
|
||||
"libraryId": None,
|
||||
"type": "IMAGE",
|
||||
"originalPath": (
|
||||
"upload/upload/e7ef5713-9dab-4bd4-b899"
|
||||
"-715b0ca4379e/b4/b8/"
|
||||
"b4b8ef00-8a6d-4056-91ff-7f86dc66e427.jpg"
|
||||
),
|
||||
"originalFileName": "filename.jpg",
|
||||
"originalMimeType": "image/jpeg",
|
||||
"thumbhash": "1igGFALX8mVGdHc5aChJf5nxNg==",
|
||||
"fileCreatedAt": "2023-01-31T17:20:37.085+00:00",
|
||||
"fileModifiedAt": "2023-01-31T17:20:39+00:00",
|
||||
"localDateTime": "2023-01-31T18:20:37.085+00:00",
|
||||
"updatedAt": "2025-05-11T10:13:49.590401+00:00",
|
||||
"isFavorite": False,
|
||||
"isArchived": False,
|
||||
"isTrashed": False,
|
||||
"duration": "0:00:00.00000",
|
||||
"exifInfo": {},
|
||||
"livePhotoVideoId": None,
|
||||
"people": [],
|
||||
"checksum": "HJm7TVOP80S+eiYZnAhWyRaB/Yc=",
|
||||
"isOffline": False,
|
||||
"hasMetadata": True,
|
||||
"duplicateId": None,
|
||||
"resized": True,
|
||||
},
|
||||
{
|
||||
"id": "2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b",
|
||||
"deviceAssetId": "web-filename.mp4-1675185639000",
|
||||
"ownerId": "e7ef5713-9dab-4bd4-b899-715b0ca4379e",
|
||||
"deviceId": "WEB",
|
||||
"libraryId": None,
|
||||
"type": "IMAGE",
|
||||
"originalPath": (
|
||||
"upload/upload/e7ef5713-9dab-4bd4-b899"
|
||||
"-715b0ca4379e/b4/b8/"
|
||||
"b4b8ef00-8a6d-4056-eeff-7f86dc66e427.mp4"
|
||||
),
|
||||
"originalFileName": "filename.mp4",
|
||||
"originalMimeType": "video/mp4",
|
||||
"thumbhash": "1igGFALX8mVGdHc5aChJf5nxNg==",
|
||||
"fileCreatedAt": "2023-01-31T17:20:37.085+00:00",
|
||||
"fileModifiedAt": "2023-01-31T17:20:39+00:00",
|
||||
"localDateTime": "2023-01-31T18:20:37.085+00:00",
|
||||
"updatedAt": "2025-05-11T10:13:49.590401+00:00",
|
||||
"isFavorite": False,
|
||||
"isArchived": False,
|
||||
"isTrashed": False,
|
||||
"duration": "0:00:00.00000",
|
||||
"exifInfo": {},
|
||||
"livePhotoVideoId": None,
|
||||
"people": [],
|
||||
"checksum": "HJm7TVOP80S+eiYZnAhWyRaB/Yc=",
|
||||
"isOffline": False,
|
||||
"hasMetadata": True,
|
||||
"duplicateId": None,
|
||||
"resized": True,
|
||||
},
|
||||
],
|
||||
"id": "721e1a4b-aa12-441e-8d3b-5ac7ab283bb6",
|
||||
"albumName": "My Album",
|
||||
"albumThumbnailAssetId": "0d03a7ad-ddc7-45a6-adee-68d322a6d2f5",
|
||||
"albumUsers": [],
|
||||
"assetCount": 1,
|
||||
"createdAt": "2025-05-11T10:13:22.799Z",
|
||||
"hasSharedLink": False,
|
||||
"isActivityEnabled": False,
|
||||
"ownerId": "e7ef5713-9dab-4bd4-b899-715b0ca4379e",
|
||||
"owner": {
|
||||
"id": "e7ef5713-9dab-4bd4-b899-715b0ca4379e",
|
||||
"email": "admin@immich.local",
|
||||
"name": "admin",
|
||||
"profileImagePath": "",
|
||||
"avatarColor": "primary",
|
||||
"profileChangedAt": "2025-05-11T10:07:46.866Z",
|
||||
},
|
||||
"shared": False,
|
||||
"updatedAt": "2025-05-17T11:26:03.696Z",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
MOCK_ALBUM_ASSETS = [
|
||||
ImmichAsset.from_dict(
|
||||
{
|
||||
"id": "2e94c203-50aa-4ad2-8e29-56dd74e0eff4",
|
||||
"deviceAssetId": "web-filename.jpg-1675185639000",
|
||||
"ownerId": "e7ef5713-9dab-4bd4-b899-715b0ca4379e",
|
||||
"deviceId": "WEB",
|
||||
"libraryId": None,
|
||||
"type": "IMAGE",
|
||||
"originalPath": (
|
||||
"upload/upload/e7ef5713-9dab-4bd4-b899"
|
||||
"-715b0ca4379e/b4/b8/"
|
||||
"b4b8ef00-8a6d-4056-91ff-7f86dc66e427.jpg"
|
||||
),
|
||||
"originalFileName": "filename.jpg",
|
||||
"originalMimeType": "image/jpeg",
|
||||
"thumbhash": "1igGFALX8mVGdHc5aChJf5nxNg==",
|
||||
"fileCreatedAt": "2023-01-31T17:20:37.085+00:00",
|
||||
"fileModifiedAt": "2023-01-31T17:20:39+00:00",
|
||||
"localDateTime": "2023-01-31T18:20:37.085+00:00",
|
||||
"updatedAt": "2025-05-11T10:13:49.590401+00:00",
|
||||
"isFavorite": False,
|
||||
"isArchived": False,
|
||||
"isTrashed": False,
|
||||
"duration": "0:00:00.00000",
|
||||
"exifInfo": {},
|
||||
"livePhotoVideoId": None,
|
||||
"people": [],
|
||||
"checksum": "HJm7TVOP80S+eiYZnAhWyRaB/Yc=",
|
||||
"isOffline": False,
|
||||
"hasMetadata": True,
|
||||
"duplicateId": None,
|
||||
"resized": True,
|
||||
}
|
||||
),
|
||||
ImmichAsset.from_dict(
|
||||
{
|
||||
"id": "2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b",
|
||||
"deviceAssetId": "web-filename.mp4-1675185639000",
|
||||
"ownerId": "e7ef5713-9dab-4bd4-b899-715b0ca4379e",
|
||||
"deviceId": "WEB",
|
||||
"libraryId": None,
|
||||
"type": "IMAGE",
|
||||
"originalPath": (
|
||||
"upload/upload/e7ef5713-9dab-4bd4-b899"
|
||||
"-715b0ca4379e/b4/b8/"
|
||||
"b4b8ef00-8a6d-4056-eeff-7f86dc66e427.mp4"
|
||||
),
|
||||
"originalFileName": "filename.mp4",
|
||||
"originalMimeType": "video/mp4",
|
||||
"thumbhash": "1igGFALX8mVGdHc5aChJf5nxNg==",
|
||||
"fileCreatedAt": "2023-01-31T17:20:37.085+00:00",
|
||||
"fileModifiedAt": "2023-01-31T17:20:39+00:00",
|
||||
"localDateTime": "2023-01-31T18:20:37.085+00:00",
|
||||
"updatedAt": "2025-05-11T10:13:49.590401+00:00",
|
||||
"isFavorite": False,
|
||||
"isArchived": False,
|
||||
"isTrashed": False,
|
||||
"duration": "0:00:00.00000",
|
||||
"exifInfo": {},
|
||||
"livePhotoVideoId": None,
|
||||
"people": [],
|
||||
"checksum": "HJm7TVOP80S+eiYZnAhWyRaB/Yc=",
|
||||
"isOffline": False,
|
||||
"hasMetadata": True,
|
||||
"duplicateId": None,
|
||||
"resized": True,
|
||||
}
|
||||
),
|
||||
]
|
||||
MOCK_PEOPLE_ASSETS = [
|
||||
ImmichAsset.from_dict(
|
||||
{
|
||||
|
||||
@@ -293,7 +293,7 @@ async def test_browse_media_collections_error(
|
||||
@pytest.mark.parametrize(
|
||||
("collection", "mocked_get_fn"),
|
||||
[
|
||||
("albums", ("albums", "async_get_album_info")),
|
||||
("albums", ("search", "async_get_all_by_album_ids")),
|
||||
("favorites", ("search", "async_get_all_favorites")),
|
||||
("people", ("search", "async_get_all_by_person_ids")),
|
||||
("tags", ("search", "async_get_all_by_tag_ids")),
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from aiohttp import ContentTypeError, RequestInfo
|
||||
from multidict import CIMultiDict, CIMultiDictProxy
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
from yarl import URL
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -43,3 +46,32 @@ async def test_admin_sensors(
|
||||
assert hass.states.get("sensor.mock_title_videos_count") is None
|
||||
assert hass.states.get("sensor.mock_title_disk_used_by_photos") is None
|
||||
assert hass.states.get("sensor.mock_title_disk_used_by_videos") is None
|
||||
|
||||
|
||||
async def test_update_error_does_not_leak_api_key(
|
||||
hass: HomeAssistant,
|
||||
mock_immich: Mock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test that API key is not leaked in error logs on connection failure."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
api_key = "SECRET_API_KEY_12345"
|
||||
headers = CIMultiDictProxy(
|
||||
CIMultiDict({"x-api-key": api_key, "Host": "example.com"})
|
||||
)
|
||||
request_info = RequestInfo(
|
||||
url=URL("https://example.com/api/server/about"),
|
||||
method="GET",
|
||||
headers=headers,
|
||||
real_url=URL("https://example.com/api/server/about"),
|
||||
)
|
||||
mock_immich.server.async_get_about_info.side_effect = ContentTypeError(
|
||||
request_info, (), status=503, message="Service Unavailable"
|
||||
)
|
||||
|
||||
await hass.config_entries.async_reload(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert api_key not in caplog.text
|
||||
|
||||
@@ -82,7 +82,7 @@ async def test_upload_file_to_album(
|
||||
|
||||
mock_immich.assets.async_upload_asset.assert_called_with("/media/screenshot.jpg")
|
||||
mock_immich.albums.async_get_album_info.assert_called_with(
|
||||
"721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", True
|
||||
"721e1a4b-aa12-441e-8d3b-5ac7ab283bb6"
|
||||
)
|
||||
mock_immich.albums.async_add_assets_to_album.assert_called_with(
|
||||
"721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", ["abcdef-0123456789"]
|
||||
|
||||
@@ -13,7 +13,13 @@ import pytest
|
||||
from homeassistant.components import influxdb
|
||||
from homeassistant.components.influxdb.const import DEFAULT_BUCKET, DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import PERCENTAGE, STATE_OFF, STATE_ON, STATE_STANDBY
|
||||
from homeassistant.const import (
|
||||
CONF_PATH,
|
||||
PERCENTAGE,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
STATE_STANDBY,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, split_entity_id
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.setup import async_setup_component
|
||||
@@ -321,6 +327,46 @@ async def test_setup_config_ssl(
|
||||
assert expected_client_args.items() <= mock_client.call_args.kwargs.items()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("mock_client", "config_ext", "expected_path"),
|
||||
[
|
||||
pytest.param(
|
||||
influxdb.DEFAULT_API_VERSION,
|
||||
{CONF_PATH: "/"},
|
||||
None,
|
||||
id="root_path_excluded",
|
||||
),
|
||||
pytest.param(
|
||||
influxdb.DEFAULT_API_VERSION,
|
||||
{CONF_PATH: "/custom_path"},
|
||||
"/custom_path",
|
||||
id="custom_path_included",
|
||||
),
|
||||
pytest.param(
|
||||
influxdb.DEFAULT_API_VERSION,
|
||||
{},
|
||||
None,
|
||||
id="no_path_excluded",
|
||||
),
|
||||
],
|
||||
indirect=["mock_client"],
|
||||
)
|
||||
async def test_setup_config_path(
|
||||
hass: HomeAssistant, mock_client, config_ext: dict, expected_path: str | None
|
||||
) -> None:
|
||||
"""Test that path='/' is not passed to InfluxDBClient, but other paths are."""
|
||||
config = BASE_V1_CONFIG.copy()
|
||||
config.update(config_ext)
|
||||
|
||||
mock_entry = MockConfigEntry(domain=DOMAIN, data=config)
|
||||
mock_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(mock_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_client.call_args.kwargs.get(CONF_PATH) == expected_path
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("mock_client", "get_write_api", "config_ext"),
|
||||
[
|
||||
|
||||
@@ -248,7 +248,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'duration',
|
||||
'friendly_name': 'MR012345 Prebrew off time',
|
||||
'max': 10,
|
||||
'max': 30,
|
||||
'min': 0,
|
||||
'mode': <NumberMode.AUTO: 'auto'>,
|
||||
'step': 0.1,
|
||||
@@ -269,7 +269,7 @@
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': 10,
|
||||
'max': 30,
|
||||
'min': 0,
|
||||
'mode': <NumberMode.AUTO: 'auto'>,
|
||||
'step': 0.1,
|
||||
@@ -309,7 +309,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'duration',
|
||||
'friendly_name': 'MR012345 Prebrew on time',
|
||||
'max': 10,
|
||||
'max': 30,
|
||||
'min': 0,
|
||||
'mode': <NumberMode.AUTO: 'auto'>,
|
||||
'step': 0.1,
|
||||
@@ -330,7 +330,7 @@
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': 10,
|
||||
'max': 30,
|
||||
'min': 0,
|
||||
'mode': <NumberMode.AUTO: 'auto'>,
|
||||
'step': 0.1,
|
||||
@@ -370,7 +370,7 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'duration',
|
||||
'friendly_name': 'MR012345 Preinfusion time',
|
||||
'max': 10,
|
||||
'max': 30,
|
||||
'min': 0,
|
||||
'mode': <NumberMode.AUTO: 'auto'>,
|
||||
'step': 0.1,
|
||||
@@ -391,7 +391,7 @@
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': 10,
|
||||
'max': 30,
|
||||
'min': 0,
|
||||
'mode': <NumberMode.AUTO: 'auto'>,
|
||||
'step': 0.1,
|
||||
|
||||
@@ -109,6 +109,32 @@ async def test_fan_set_speed(
|
||||
)
|
||||
|
||||
|
||||
async def test_fan_set_percentage_no_op_when_already_at_target(
|
||||
hass: HomeAssistant,
|
||||
mock_miele_client: MagicMock,
|
||||
setup_platform: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test that set_percentage is a no-op when already at the target step."""
|
||||
await hass.services.async_call(
|
||||
FAN_DOMAIN,
|
||||
SERVICE_SET_PERCENTAGE,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_PERCENTAGE: 50},
|
||||
blocking=True,
|
||||
)
|
||||
mock_miele_client.send_action.assert_called_once_with(
|
||||
"DummyAppliance_18", {"ventilationStep": 2}
|
||||
)
|
||||
mock_miele_client.send_action.reset_mock()
|
||||
|
||||
await hass.services.async_call(
|
||||
FAN_DOMAIN,
|
||||
SERVICE_SET_PERCENTAGE,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_PERCENTAGE: 50},
|
||||
blocking=True,
|
||||
)
|
||||
mock_miele_client.send_action.assert_not_called()
|
||||
|
||||
|
||||
async def test_fan_turn_on_w_percentage(
|
||||
hass: HomeAssistant,
|
||||
mock_miele_client: MagicMock,
|
||||
|
||||
@@ -625,9 +625,7 @@ async def test_hassio_confirm(
|
||||
assert result["description_placeholders"] == {"addon": "Mosquitto Mqtt Broker"}
|
||||
|
||||
mock_try_connection_success.reset_mock()
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {"discovery": True}
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["result"].data == {
|
||||
@@ -636,7 +634,6 @@ async def test_hassio_confirm(
|
||||
"port": 1883,
|
||||
"username": "mock-user",
|
||||
"password": "mock-pass",
|
||||
"discovery": True,
|
||||
}
|
||||
# Check we tried the connection
|
||||
assert len(mock_try_connection_success.mock_calls)
|
||||
@@ -673,9 +670,7 @@ async def test_hassio_cannot_connect(
|
||||
assert result["description_placeholders"] == {"addon": "Mock Addon"}
|
||||
|
||||
mock_try_connection_time_out.reset_mock()
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {"discovery": True}
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"]["base"] == "cannot_connect"
|
||||
@@ -732,7 +727,6 @@ async def test_addon_flow_with_supervisor_addon_running(
|
||||
"port": 1883,
|
||||
"username": "mock-user",
|
||||
"password": "mock-pass",
|
||||
"discovery": True,
|
||||
}
|
||||
# Check we tried the connection
|
||||
assert len(mock_try_connection_success.mock_calls)
|
||||
@@ -800,7 +794,6 @@ async def test_addon_flow_with_supervisor_addon_installed(
|
||||
"port": 1883,
|
||||
"username": "mock-user",
|
||||
"password": "mock-pass",
|
||||
"discovery": True,
|
||||
}
|
||||
# Check we tried the connection
|
||||
assert len(mock_try_connection_success.mock_calls)
|
||||
@@ -1040,7 +1033,6 @@ async def test_addon_flow_with_supervisor_addon_not_installed(
|
||||
"port": 1883,
|
||||
"username": "mock-user",
|
||||
"password": "mock-pass",
|
||||
"discovery": True,
|
||||
}
|
||||
# Check we tried the connection
|
||||
assert len(mock_try_connection_success.mock_calls)
|
||||
|
||||
@@ -63,6 +63,7 @@ def mock_diffuser(
|
||||
diffuser_mock.version = version
|
||||
diffuser_mock.wifi_percentage = wifi_percentage
|
||||
diffuser_mock.data = load_json_object_fixture("data.json", DOMAIN)
|
||||
diffuser_mock.hub_data = diffuser_mock.data["hub"]
|
||||
return diffuser_mock
|
||||
|
||||
|
||||
@@ -71,6 +72,24 @@ def mock_diffuser_v1_battery_cartridge() -> MagicMock:
|
||||
return mock_diffuser(hublot="lot123v1")
|
||||
|
||||
|
||||
def mock_diffuser_v3_no_battery_no_fill() -> MagicMock:
|
||||
"""Create and return a mock version 3 Diffuser without battery or fill sensor."""
|
||||
diffuser = mock_diffuser(
|
||||
hublot="lot123v3",
|
||||
battery_percentage=Exception(),
|
||||
charging=Exception(),
|
||||
fill="",
|
||||
has_battery=False,
|
||||
has_cartridge=True,
|
||||
name="Genie V3",
|
||||
perfume="Ritual of Sakura",
|
||||
version="6.0",
|
||||
)
|
||||
diffuser.data = load_json_object_fixture("data_no_fill.json", DOMAIN)
|
||||
diffuser.hub_data = diffuser.data["hub"]
|
||||
return diffuser
|
||||
|
||||
|
||||
def mock_diffuser_v2_no_battery_no_cartridge() -> MagicMock:
|
||||
"""Create and return a mock version 2 Diffuser without battery and cartridge."""
|
||||
return mock_diffuser(
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"hub": {
|
||||
"hublot": "LOT456",
|
||||
"hash": "abcdef1234567890ghijklmnopqrstuvwxyz",
|
||||
"status": 1,
|
||||
"title": null,
|
||||
"current_time": "2023-06-09T20:50",
|
||||
"cached_time": "2023-06-09T20:48",
|
||||
"ping_update": "25",
|
||||
"attributes": {
|
||||
"roomc": "4",
|
||||
"speedc": "3",
|
||||
"fanc": "1",
|
||||
"roomnamec": "Living room",
|
||||
"resetc": "",
|
||||
"fspacenamec": "",
|
||||
"fspacetypec": ""
|
||||
},
|
||||
"sensors": {
|
||||
"wific": {
|
||||
"id": 10,
|
||||
"sensor_id": 1,
|
||||
"title": "High",
|
||||
"description": "",
|
||||
"icon": "icon-signal.png",
|
||||
"image": "",
|
||||
"discover_image": "",
|
||||
"discover_url": null,
|
||||
"min_value": "-69.99",
|
||||
"max_value": "-0.00",
|
||||
"interval": "1",
|
||||
"created_at": "2017-03-10 16:17:30",
|
||||
"updated_at": "2020-06-17 16:57:53",
|
||||
"default": 0
|
||||
},
|
||||
"rfidc": {
|
||||
"id": 54,
|
||||
"sensor_id": 4,
|
||||
"title": "Private Collection Sweet Jasmine",
|
||||
"description": "",
|
||||
"icon": "icon-jasmine.png",
|
||||
"image": "background-jasmine.png",
|
||||
"discover_image": "discover-jasmine.png",
|
||||
"discover_url": "sweet-jasmine-cartridge-1105402.html",
|
||||
"min_value": "05377650",
|
||||
"max_value": "05377650",
|
||||
"interval": "",
|
||||
"created_at": "2019-04-04 07:53:32",
|
||||
"updated_at": "2021-01-26 14:17:03",
|
||||
"default": 0
|
||||
},
|
||||
"versionc": "6.0",
|
||||
"ipc": "1682963060"
|
||||
},
|
||||
"settings": [
|
||||
{
|
||||
"schedule_id": 1835730,
|
||||
"start": "07:30",
|
||||
"end": "08:30",
|
||||
"mon": 1,
|
||||
"tue": 1,
|
||||
"wed": 1,
|
||||
"thu": 1,
|
||||
"fri": 1,
|
||||
"sat": 1,
|
||||
"sun": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ from .common import (
|
||||
init_integration,
|
||||
mock_config_entry,
|
||||
mock_diffuser_v1_battery_cartridge,
|
||||
mock_diffuser_v3_no_battery_no_fill,
|
||||
)
|
||||
|
||||
|
||||
@@ -63,3 +64,19 @@ async def test_sensors_diffuser_v1_battery_cartridge(
|
||||
assert entry
|
||||
assert entry.unique_id == f"{hublot}-wifi_percentage"
|
||||
assert entry.entity_category == EntityCategory.DIAGNOSTIC
|
||||
|
||||
|
||||
async def test_sensors_diffuser_v3_no_fill(
|
||||
hass: HomeAssistant, entity_registry: er.EntityRegistry
|
||||
) -> None:
|
||||
"""Test that a Genie 3.0 without fill sensor data does not create a fill entity."""
|
||||
config_entry = mock_config_entry(unique_id="id_123_sensor_test_diffuser_v3")
|
||||
diffuser = mock_diffuser_v3_no_battery_no_fill()
|
||||
await init_integration(hass, config_entry, [diffuser])
|
||||
|
||||
assert hass.states.get("sensor.genie_v3_fill") is None
|
||||
assert entity_registry.async_get("sensor.genie_v3_fill") is None
|
||||
|
||||
state = hass.states.get("sensor.genie_v3_perfume")
|
||||
assert state
|
||||
assert state.state == diffuser.perfume
|
||||
|
||||
@@ -49,6 +49,21 @@ async def test_base_station_migration(
|
||||
assert device_registry.async_get_device(identifiers=new_identifiers) is not None
|
||||
|
||||
|
||||
async def test_base_station_model_is_string(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
config_entry: MockConfigEntry,
|
||||
patch_simplisafe_api,
|
||||
) -> None:
|
||||
"""Test that the base station model is stored as a string in the device registry."""
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
device = device_registry.async_get_device(identifiers={(DOMAIN, "12345")})
|
||||
assert device is not None
|
||||
assert isinstance(device.model, str)
|
||||
|
||||
|
||||
async def test_coordinator_update_triggers_reauth_on_invalid_credentials(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
|
||||
@@ -6,6 +6,14 @@
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'source_list': list([
|
||||
'66 - Watercolors',
|
||||
"Les P'tits Bateaux",
|
||||
'James Taylor Radio',
|
||||
'1984',
|
||||
'American Tall Tales',
|
||||
'sample playlist',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
@@ -49,6 +57,14 @@
|
||||
'media_content_type': <MediaType.MUSIC: 'music'>,
|
||||
'repeat': <RepeatMode.OFF: 'off'>,
|
||||
'shuffle': False,
|
||||
'source_list': list([
|
||||
'66 - Watercolors',
|
||||
"Les P'tits Bateaux",
|
||||
'James Taylor Radio',
|
||||
'1984',
|
||||
'American Tall Tales',
|
||||
'sample playlist',
|
||||
]),
|
||||
'supported_features': <MediaPlayerEntityFeature: 8321599>,
|
||||
'volume_level': 0.19,
|
||||
}),
|
||||
|
||||
@@ -40,6 +40,7 @@ from homeassistant.components.media_player import (
|
||||
SERVICE_PLAY_MEDIA,
|
||||
SERVICE_SELECT_SOURCE,
|
||||
MediaPlayerEnqueue,
|
||||
MediaPlayerEntityFeature,
|
||||
RepeatMode,
|
||||
)
|
||||
from homeassistant.components.sonos.const import (
|
||||
@@ -723,6 +724,7 @@ async def test_play_sonos_playlist(
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("speaker_model", ["Sonos Amp"], indirect=True)
|
||||
async def test_select_source_line_in_tv(
|
||||
hass: HomeAssistant,
|
||||
soco_factory: SoCoMockFactory,
|
||||
@@ -825,6 +827,7 @@ async def test_select_source_line_in_tv(
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("speaker_model", ["Sonos Amp"], indirect=True)
|
||||
async def test_select_source_play_uri(
|
||||
hass: HomeAssistant,
|
||||
soco_factory: SoCoMockFactory,
|
||||
@@ -866,6 +869,7 @@ async def test_select_source_play_uri(
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("speaker_model", ["Sonos Amp"], indirect=True)
|
||||
async def test_select_source_play_queue(
|
||||
hass: HomeAssistant,
|
||||
soco_factory: SoCoMockFactory,
|
||||
@@ -897,6 +901,7 @@ async def test_select_source_play_queue(
|
||||
soco_mock.play_from_queue.assert_called_with(0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("speaker_model", ["Sonos Amp"], indirect=True)
|
||||
async def test_select_source_error(
|
||||
hass: HomeAssistant,
|
||||
soco_factory: SoCoMockFactory,
|
||||
@@ -1425,16 +1430,26 @@ async def test_media_get_queue(
|
||||
assert result == snapshot
|
||||
|
||||
|
||||
FAVORITE_TITLES = [
|
||||
"66 - Watercolors",
|
||||
"Les P'tits Bateaux",
|
||||
"James Taylor Radio",
|
||||
"1984",
|
||||
"American Tall Tales",
|
||||
"sample playlist",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("speaker_model", "source_list"),
|
||||
[
|
||||
("Sonos Arc Ultra", [SOURCE_TV]),
|
||||
("Sonos Arc", [SOURCE_TV]),
|
||||
("Sonos Playbar", [SOURCE_TV]),
|
||||
("Sonos Connect", [SOURCE_LINEIN]),
|
||||
("Sonos Play:5", [SOURCE_LINEIN]),
|
||||
("Sonos Amp", [SOURCE_LINEIN, SOURCE_TV]),
|
||||
("Sonos Era", None),
|
||||
("Sonos Arc Ultra", [SOURCE_TV, *FAVORITE_TITLES]),
|
||||
("Sonos Arc", [SOURCE_TV, *FAVORITE_TITLES]),
|
||||
("Sonos Playbar", [SOURCE_TV, *FAVORITE_TITLES]),
|
||||
("Sonos Connect", [SOURCE_LINEIN, *FAVORITE_TITLES]),
|
||||
("Sonos Play:5", [SOURCE_LINEIN, *FAVORITE_TITLES]),
|
||||
("Sonos Amp", [SOURCE_LINEIN, SOURCE_TV, *FAVORITE_TITLES]),
|
||||
("Sonos Era", FAVORITE_TITLES),
|
||||
],
|
||||
indirect=["speaker_model"],
|
||||
)
|
||||
@@ -1442,11 +1457,69 @@ async def test_media_source_list(
|
||||
hass: HomeAssistant,
|
||||
async_autosetup_sonos,
|
||||
speaker_model: str,
|
||||
source_list: list[str] | None,
|
||||
source_list: list[str],
|
||||
) -> None:
|
||||
"""Test the mapping between the speaker model name and source_list."""
|
||||
state = hass.states.get("media_player.zone_a")
|
||||
assert state.attributes.get(ATTR_INPUT_SOURCE_LIST) == source_list
|
||||
features = MediaPlayerEntityFeature(state.attributes["supported_features"])
|
||||
assert features & MediaPlayerEntityFeature.SELECT_SOURCE
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("speaker_model", "expected_sources", "expected_after_clear"),
|
||||
[
|
||||
("Model Name", FAVORITE_TITLES, None),
|
||||
("Sonos Arc Ultra", [SOURCE_TV, *FAVORITE_TITLES], [SOURCE_TV]),
|
||||
(
|
||||
"Sonos Amp",
|
||||
[SOURCE_LINEIN, SOURCE_TV, *FAVORITE_TITLES],
|
||||
[SOURCE_LINEIN, SOURCE_TV],
|
||||
),
|
||||
],
|
||||
indirect=["speaker_model"],
|
||||
)
|
||||
async def test_source_list_favorites_cleared(
|
||||
hass: HomeAssistant,
|
||||
soco: MockSoCo,
|
||||
async_autosetup_sonos,
|
||||
speaker_model: str,
|
||||
expected_sources: list[str],
|
||||
expected_after_clear: list[str] | None,
|
||||
) -> None:
|
||||
"""Test source_list and SELECT_SOURCE update when favorites are cleared."""
|
||||
entity_id = "media_player.zone_a"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.attributes.get(ATTR_INPUT_SOURCE_LIST) == expected_sources
|
||||
features = MediaPlayerEntityFeature(state.attributes["supported_features"])
|
||||
assert features & MediaPlayerEntityFeature.SELECT_SOURCE
|
||||
|
||||
# Clear favorites via the music library mock
|
||||
empty_favorites = SearchResult([], "favorites", 0, 0, 2)
|
||||
soco.music_library.get_sonos_favorites.return_value = empty_favorites
|
||||
soco.music_library.get_music_library_information.side_effect = None
|
||||
soco.music_library.get_music_library_information.return_value = SearchResult(
|
||||
[], "sonos_playlists", 0, 0, 0
|
||||
)
|
||||
|
||||
# Trigger a favorites cache update via the content directory event
|
||||
service = soco.contentDirectory
|
||||
subscription = service.subscribe.return_value
|
||||
favorites_event = SonosMockEvent(
|
||||
soco,
|
||||
service,
|
||||
{"favorites_update_id": "2", "container_update_i_ds": "FV:2,2"},
|
||||
)
|
||||
subscription.callback(event=favorites_event)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.attributes.get(ATTR_INPUT_SOURCE_LIST) == expected_after_clear
|
||||
features = MediaPlayerEntityFeature(state.attributes["supported_features"])
|
||||
assert bool(features & MediaPlayerEntityFeature.SELECT_SOURCE) == (
|
||||
expected_after_clear is not None
|
||||
)
|
||||
|
||||
|
||||
async def test_service_update_alarm(
|
||||
|
||||
@@ -123,6 +123,25 @@ async def test_device_registry(
|
||||
assert reg_device == snapshot
|
||||
|
||||
|
||||
async def test_device_registry_numeric_firmware(
|
||||
hass: HomeAssistant,
|
||||
device_registry: DeviceRegistry,
|
||||
config_entry: MockConfigEntry,
|
||||
lms: MagicMock,
|
||||
) -> None:
|
||||
"""Test that numeric firmware values are stored as strings in the device registry."""
|
||||
players = await lms.async_get_players()
|
||||
players[0].firmware = 137
|
||||
|
||||
with patch("homeassistant.components.squeezebox.Server", return_value=lms):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
reg_device = device_registry.async_get_device(identifiers={(DOMAIN, TEST_MAC[0])})
|
||||
assert reg_device is not None
|
||||
assert reg_device.hw_version == "137"
|
||||
|
||||
|
||||
async def test_device_registry_server_merged(
|
||||
hass: HomeAssistant,
|
||||
device_registry: DeviceRegistry,
|
||||
|
||||
@@ -6,6 +6,7 @@ from homeassistant.components.subaru.const import (
|
||||
API_GEN_1,
|
||||
API_GEN_2,
|
||||
API_GEN_3,
|
||||
API_GEN_4,
|
||||
VEHICLE_API_GEN,
|
||||
VEHICLE_HAS_EV,
|
||||
VEHICLE_HAS_REMOTE_SERVICE,
|
||||
@@ -21,6 +22,7 @@ from homeassistant.components.subaru.const import (
|
||||
TEST_VIN_1_G1 = "JF2ABCDE6L0000001"
|
||||
TEST_VIN_2_EV = "JF2ABCDE6L0000002"
|
||||
TEST_VIN_3_G3 = "JF2ABCDE6L0000003"
|
||||
TEST_VIN_4_G4 = "JF2ABCDE6L0000004"
|
||||
|
||||
VEHICLE_DATA = {
|
||||
TEST_VIN_1_G1: {
|
||||
@@ -56,6 +58,17 @@ VEHICLE_DATA = {
|
||||
VEHICLE_HAS_REMOTE_SERVICE: True,
|
||||
VEHICLE_HAS_SAFETY_SERVICE: True,
|
||||
},
|
||||
TEST_VIN_4_G4: {
|
||||
VEHICLE_VIN: TEST_VIN_4_G4,
|
||||
VEHICLE_MODEL_YEAR: "2026",
|
||||
VEHICLE_MODEL_NAME: "Outback",
|
||||
VEHICLE_NAME: "test_vehicle_4",
|
||||
VEHICLE_HAS_EV: False,
|
||||
VEHICLE_API_GEN: API_GEN_4,
|
||||
VEHICLE_HAS_REMOTE_START: True,
|
||||
VEHICLE_HAS_REMOTE_SERVICE: True,
|
||||
VEHICLE_HAS_SAFETY_SERVICE: True,
|
||||
},
|
||||
}
|
||||
|
||||
MOCK_DATETIME = datetime.fromtimestamp(1595560000, UTC)
|
||||
|
||||
@@ -12,12 +12,14 @@ from homeassistant.components.subaru.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .api_responses import (
|
||||
TEST_VIN_1_G1,
|
||||
TEST_VIN_2_EV,
|
||||
TEST_VIN_3_G3,
|
||||
TEST_VIN_4_G4,
|
||||
VEHICLE_DATA,
|
||||
VEHICLE_STATUS_EV,
|
||||
VEHICLE_STATUS_G3,
|
||||
@@ -58,6 +60,29 @@ async def test_setup_g3(hass: HomeAssistant, subaru_config_entry) -> None:
|
||||
assert check_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
|
||||
async def test_setup_g4(hass: HomeAssistant, subaru_config_entry) -> None:
|
||||
"""Test setup with a G4 vehicle (2026+ models report api_gen "g4")."""
|
||||
await setup_subaru_config_entry(
|
||||
hass,
|
||||
subaru_config_entry,
|
||||
vehicle_list=[TEST_VIN_4_G4],
|
||||
vehicle_data=VEHICLE_DATA[TEST_VIN_4_G4],
|
||||
vehicle_status=VEHICLE_STATUS_G3,
|
||||
)
|
||||
check_entry = hass.config_entries.async_get_entry(subaru_config_entry.entry_id)
|
||||
assert check_entry
|
||||
assert check_entry.state is ConfigEntryState.LOADED
|
||||
# Gen4 must receive both Gen2+ and Gen3+ sensor sets; without this, only
|
||||
# the odometer was created on 2026 model year vehicles.
|
||||
entity_registry = er.async_get(hass)
|
||||
assert entity_registry.async_get_entity_id(
|
||||
"sensor", DOMAIN, f"{TEST_VIN_4_G4}_AVG_FUEL_CONSUMPTION"
|
||||
)
|
||||
assert entity_registry.async_get_entity_id(
|
||||
"sensor", DOMAIN, f"{TEST_VIN_4_G4}_REMAINING_FUEL_PERCENT"
|
||||
)
|
||||
|
||||
|
||||
async def test_setup_g1(hass: HomeAssistant, subaru_config_entry) -> None:
|
||||
"""Test setup with a G1 vehicle."""
|
||||
await setup_subaru_config_entry(
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Test for the switchbot_cloud base entity."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.components.switchbot_cloud.const import DOMAIN
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from . import METER_INFO, configure_integration
|
||||
|
||||
|
||||
async def test_sw_version_cast_to_string(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
mock_list_devices,
|
||||
mock_get_status,
|
||||
mock_setup_webhook,
|
||||
) -> None:
|
||||
"""Test the device sw_version is a string when the API returns an int."""
|
||||
mock_list_devices.return_value = [METER_INFO]
|
||||
mock_get_status.return_value = {"version": 123}
|
||||
|
||||
with patch("homeassistant.components.switchbot_cloud.PLATFORMS", [Platform.SENSOR]):
|
||||
await configure_integration(hass)
|
||||
|
||||
device = device_registry.async_get_device(
|
||||
identifiers={(DOMAIN, METER_INFO.device_id)}
|
||||
)
|
||||
assert device is not None
|
||||
assert device.sw_version == "123"
|
||||
|
||||
|
||||
async def test_sw_version_none_when_missing(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
mock_list_devices,
|
||||
mock_get_status,
|
||||
mock_setup_webhook,
|
||||
) -> None:
|
||||
"""Test the device sw_version is None when the API omits the version."""
|
||||
mock_list_devices.return_value = [METER_INFO]
|
||||
mock_get_status.return_value = {}
|
||||
|
||||
with patch("homeassistant.components.switchbot_cloud.PLATFORMS", [Platform.SENSOR]):
|
||||
await configure_integration(hass)
|
||||
|
||||
device = device_registry.async_get_device(
|
||||
identifiers={(DOMAIN, METER_INFO.device_id)}
|
||||
)
|
||||
assert device is not None
|
||||
assert device.sw_version is None
|
||||
@@ -2,7 +2,12 @@
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from aiowebdav2.exceptions import AccessDeniedError, UnauthorizedError
|
||||
from aiowebdav2.exceptions import (
|
||||
AccessDeniedError,
|
||||
ConnectionExceptionError,
|
||||
NoConnectionError,
|
||||
UnauthorizedError,
|
||||
)
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.webdav.const import CONF_BACKUP_PATH, DOMAIN
|
||||
@@ -28,8 +33,29 @@ from tests.common import MockConfigEntry
|
||||
"Access denied to /access_denied",
|
||||
ConfigEntryState.SETUP_ERROR,
|
||||
),
|
||||
(
|
||||
ConnectionExceptionError(ConnectionError("Connection refused")),
|
||||
"Connection refused",
|
||||
ConfigEntryState.SETUP_RETRY,
|
||||
),
|
||||
(
|
||||
NoConnectionError("webdav.demo"),
|
||||
"No connection with webdav.demo",
|
||||
ConfigEntryState.SETUP_RETRY,
|
||||
),
|
||||
(
|
||||
TimeoutError(),
|
||||
"",
|
||||
ConfigEntryState.SETUP_RETRY,
|
||||
),
|
||||
],
|
||||
ids=[
|
||||
"UnauthorizedError",
|
||||
"AccessDeniedError",
|
||||
"ConnectionExceptionError",
|
||||
"NoConnectionError",
|
||||
"TimeoutError",
|
||||
],
|
||||
ids=["UnauthorizedError", "AccessDeniedError"],
|
||||
)
|
||||
async def test_error_during_setup(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
from weheat.exceptions import ApiException
|
||||
|
||||
from homeassistant.components.weheat.const import (
|
||||
DOMAIN,
|
||||
@@ -144,6 +145,35 @@ async def test_reauth(
|
||||
assert entry.unique_id == USER_UUID_1
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("current_request_with_host")
|
||||
@pytest.mark.parametrize(
|
||||
"side_effect",
|
||||
[ApiException(status=500, reason="Internal Server Error"), None],
|
||||
)
|
||||
async def test_api_error_during_create_entry(
|
||||
hass: HomeAssistant,
|
||||
hass_client_no_auth: ClientSessionGenerator,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
side_effect: Exception | None,
|
||||
) -> None:
|
||||
"""Test config flow aborts when the API call fails or returns no user."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={CONF_SOURCE: SOURCE_USER}
|
||||
)
|
||||
|
||||
await handle_oauth(hass, hass_client_no_auth, aioclient_mock, result)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.weheat.config_flow.async_get_user_id_from_token",
|
||||
side_effect=side_effect,
|
||||
return_value=None,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "oauth_failed"
|
||||
|
||||
|
||||
async def handle_oauth(
|
||||
hass: HomeAssistant,
|
||||
hass_client_no_auth: ClientSessionGenerator,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user