forked from home-assistant/core
Compare commits
57 Commits
2025.5.0b2
...
2025.5.0b8
| Author | SHA1 | Date | |
|---|---|---|---|
| 35c90d9bde | |||
| a9632bd0ff | |||
| 983e134ae9 | |||
| e217532f9e | |||
| 1eeab28eec | |||
| 2a3bd45901 | |||
| d16453a465 | |||
| de63dddc96 | |||
| ccffe19611 | |||
| 806bcf47d9 | |||
| 5ed3f18d70 | |||
| 7cc142dd59 | |||
| 9150c78901 | |||
| 4b7c337dc9 | |||
| 1aa79c71cc | |||
| 5f70140e72 | |||
| 58f7a8a51e | |||
| a91ae71139 | |||
| 576b4ef60d | |||
| 918499a85c | |||
| 46ef578986 | |||
| 86162eb660 | |||
| 7f7a33b027 | |||
| 867df99353 | |||
| 283e9d073b | |||
| 38f26376a1 | |||
| 0322dd0e0f | |||
| 3798802557 | |||
| f7833bdbd4 | |||
| e3a156c9b7 | |||
| 6247ec73a3 | |||
| 3feda06e60 | |||
| 56e895fdd4 | |||
| 541506cbdb | |||
| 1f4cda6282 | |||
| 6f77d0b0d5 | |||
| 7976e1b104 | |||
| 1c260cfb00 | |||
| 8424f179e4 | |||
| 00a14a0824 | |||
| 34bec1c50f | |||
| 1d0c520f64 | |||
| d51eda40b3 | |||
| 2d3259413a | |||
| 7a7bd9c621 | |||
| 8ce0b6b4b3 | |||
| 63679333cc | |||
| 5b12bdca00 | |||
| 99e13278e3 | |||
| 07a03ee10d | |||
| fb9f8e3581 | |||
| ee125cd9a4 | |||
| 35a1429e2b | |||
| 89916b38e9 | |||
| c560439545 | |||
| 7322be2006 | |||
| e95ed12ba1 |
Generated
+4
-4
@@ -171,6 +171,8 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/avea/ @pattyland
|
||||
/homeassistant/components/awair/ @ahayworth @danielsjf
|
||||
/tests/components/awair/ @ahayworth @danielsjf
|
||||
/homeassistant/components/aws_s3/ @tomasbedrich
|
||||
/tests/components/aws_s3/ @tomasbedrich
|
||||
/homeassistant/components/axis/ @Kane610
|
||||
/tests/components/axis/ @Kane610
|
||||
/homeassistant/components/azure_data_explorer/ @kaareseras
|
||||
@@ -1318,8 +1320,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/ruuvitag_ble/ @akx
|
||||
/homeassistant/components/rympro/ @OnFreund @elad-bar @maorcc
|
||||
/tests/components/rympro/ @OnFreund @elad-bar @maorcc
|
||||
/homeassistant/components/s3/ @tomasbedrich
|
||||
/tests/components/s3/ @tomasbedrich
|
||||
/homeassistant/components/sabnzbd/ @shaiu @jpbede
|
||||
/tests/components/sabnzbd/ @shaiu @jpbede
|
||||
/homeassistant/components/saj/ @fredericvl
|
||||
@@ -1678,8 +1678,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/vlc_telnet/ @rodripf @MartinHjelmare
|
||||
/homeassistant/components/vodafone_station/ @paoloantinori @chemelli74
|
||||
/tests/components/vodafone_station/ @paoloantinori @chemelli74
|
||||
/homeassistant/components/voip/ @balloob @synesthesiam
|
||||
/tests/components/voip/ @balloob @synesthesiam
|
||||
/homeassistant/components/voip/ @balloob @synesthesiam @jaminh
|
||||
/tests/components/voip/ @balloob @synesthesiam @jaminh
|
||||
/homeassistant/components/volumio/ @OnFreund
|
||||
/tests/components/volumio/ @OnFreund
|
||||
/homeassistant/components/volvooncall/ @molobrakos
|
||||
|
||||
+5
-5
@@ -1,10 +1,10 @@
|
||||
image: ghcr.io/home-assistant/{arch}-homeassistant
|
||||
build_from:
|
||||
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.02.1
|
||||
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.02.1
|
||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.02.1
|
||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.02.1
|
||||
i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.02.1
|
||||
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.05.0
|
||||
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.05.0
|
||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.05.0
|
||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.05.0
|
||||
i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.05.0
|
||||
codenotary:
|
||||
signer: notary@home-assistant.io
|
||||
base_image: notary@home-assistant.io
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
{
|
||||
"domain": "amazon",
|
||||
"name": "Amazon",
|
||||
"integrations": ["alexa", "amazon_polly", "aws", "fire_tv", "route53"]
|
||||
"integrations": [
|
||||
"alexa",
|
||||
"amazon_polly",
|
||||
"aws",
|
||||
"aws_s3",
|
||||
"fire_tv",
|
||||
"route53"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ POLLEN_CATEGORY_MAP = {
|
||||
2: "moderate",
|
||||
3: "high",
|
||||
4: "very_high",
|
||||
5: "extreme",
|
||||
}
|
||||
UPDATE_INTERVAL_OBSERVATION = timedelta(minutes=40)
|
||||
UPDATE_INTERVAL_DAILY_FORECAST = timedelta(hours=6)
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
"level": {
|
||||
"name": "Level",
|
||||
"state": {
|
||||
"extreme": "Extreme",
|
||||
"high": "[%key:common::state::high%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"moderate": "Moderate",
|
||||
@@ -89,6 +90,7 @@
|
||||
"level": {
|
||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||
"state": {
|
||||
"extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]",
|
||||
"high": "[%key:common::state::high%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
||||
@@ -123,6 +125,7 @@
|
||||
"level": {
|
||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||
"state": {
|
||||
"extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]",
|
||||
"high": "[%key:common::state::high%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
||||
@@ -167,6 +170,7 @@
|
||||
"level": {
|
||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||
"state": {
|
||||
"extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]",
|
||||
"high": "[%key:common::state::high%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
||||
@@ -181,6 +185,7 @@
|
||||
"level": {
|
||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||
"state": {
|
||||
"extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]",
|
||||
"high": "[%key:common::state::high%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
||||
@@ -195,6 +200,7 @@
|
||||
"level": {
|
||||
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
|
||||
"state": {
|
||||
"extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]",
|
||||
"high": "[%key:common::state::high%]",
|
||||
"low": "[%key:common::state::low%]",
|
||||
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
|
||||
|
||||
+1
-8
@@ -1,4 +1,4 @@
|
||||
"""The S3 integration."""
|
||||
"""The AWS S3 integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -7,7 +7,6 @@ from typing import cast
|
||||
|
||||
from aiobotocore.client import AioBaseClient as S3Client
|
||||
from aiobotocore.session import AioSession
|
||||
from botocore.config import Config
|
||||
from botocore.exceptions import ClientError, ConnectionError, ParamValidationError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -33,11 +32,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: S3ConfigEntry) -> bool:
|
||||
"""Set up S3 from a config entry."""
|
||||
|
||||
data = cast(dict, entry.data)
|
||||
# due to https://github.com/home-assistant/core/issues/143995
|
||||
config = Config(
|
||||
request_checksum_calculation="when_required",
|
||||
response_checksum_validation="when_required",
|
||||
)
|
||||
try:
|
||||
session = AioSession()
|
||||
# pylint: disable-next=unnecessary-dunder-call
|
||||
@@ -46,7 +40,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: S3ConfigEntry) -> bool:
|
||||
endpoint_url=data.get(CONF_ENDPOINT_URL),
|
||||
aws_secret_access_key=data[CONF_SECRET_ACCESS_KEY],
|
||||
aws_access_key_id=data[CONF_ACCESS_KEY_ID],
|
||||
config=config,
|
||||
).__aenter__()
|
||||
await client.head_bucket(Bucket=data[CONF_BUCKET])
|
||||
except ClientError as err:
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Backup platform for the S3 integration."""
|
||||
"""Backup platform for the AWS S3 integration."""
|
||||
|
||||
from collections.abc import AsyncIterator, Callable, Coroutine
|
||||
import functools
|
||||
+29
-21
@@ -1,8 +1,9 @@
|
||||
"""Config flow for the S3 integration."""
|
||||
"""Config flow for the AWS S3 integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from aiobotocore.session import AioSession
|
||||
from botocore.exceptions import ClientError, ConnectionError, ParamValidationError
|
||||
@@ -17,6 +18,7 @@ from homeassistant.helpers.selector import (
|
||||
)
|
||||
|
||||
from .const import (
|
||||
AWS_DOMAIN,
|
||||
CONF_ACCESS_KEY_ID,
|
||||
CONF_BUCKET,
|
||||
CONF_ENDPOINT_URL,
|
||||
@@ -57,28 +59,34 @@ class S3ConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
CONF_ENDPOINT_URL: user_input[CONF_ENDPOINT_URL],
|
||||
}
|
||||
)
|
||||
try:
|
||||
session = AioSession()
|
||||
async with session.create_client(
|
||||
"s3",
|
||||
endpoint_url=user_input.get(CONF_ENDPOINT_URL),
|
||||
aws_secret_access_key=user_input[CONF_SECRET_ACCESS_KEY],
|
||||
aws_access_key_id=user_input[CONF_ACCESS_KEY_ID],
|
||||
) as client:
|
||||
await client.head_bucket(Bucket=user_input[CONF_BUCKET])
|
||||
except ClientError:
|
||||
errors["base"] = "invalid_credentials"
|
||||
except ParamValidationError as err:
|
||||
if "Invalid bucket name" in str(err):
|
||||
errors[CONF_BUCKET] = "invalid_bucket_name"
|
||||
except ValueError:
|
||||
|
||||
if not urlparse(user_input[CONF_ENDPOINT_URL]).hostname.endswith(
|
||||
AWS_DOMAIN
|
||||
):
|
||||
errors[CONF_ENDPOINT_URL] = "invalid_endpoint_url"
|
||||
except ConnectionError:
|
||||
errors[CONF_ENDPOINT_URL] = "cannot_connect"
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_BUCKET], data=user_input
|
||||
)
|
||||
try:
|
||||
session = AioSession()
|
||||
async with session.create_client(
|
||||
"s3",
|
||||
endpoint_url=user_input.get(CONF_ENDPOINT_URL),
|
||||
aws_secret_access_key=user_input[CONF_SECRET_ACCESS_KEY],
|
||||
aws_access_key_id=user_input[CONF_ACCESS_KEY_ID],
|
||||
) as client:
|
||||
await client.head_bucket(Bucket=user_input[CONF_BUCKET])
|
||||
except ClientError:
|
||||
errors["base"] = "invalid_credentials"
|
||||
except ParamValidationError as err:
|
||||
if "Invalid bucket name" in str(err):
|
||||
errors[CONF_BUCKET] = "invalid_bucket_name"
|
||||
except ValueError:
|
||||
errors[CONF_ENDPOINT_URL] = "invalid_endpoint_url"
|
||||
except ConnectionError:
|
||||
errors[CONF_ENDPOINT_URL] = "cannot_connect"
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_BUCKET], data=user_input
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
@@ -1,18 +1,19 @@
|
||||
"""Constants for the S3 integration."""
|
||||
"""Constants for the AWS S3 integration."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Final
|
||||
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
DOMAIN: Final = "s3"
|
||||
DOMAIN: Final = "aws_s3"
|
||||
|
||||
CONF_ACCESS_KEY_ID = "access_key_id"
|
||||
CONF_SECRET_ACCESS_KEY = "secret_access_key"
|
||||
CONF_ENDPOINT_URL = "endpoint_url"
|
||||
CONF_BUCKET = "bucket"
|
||||
|
||||
DEFAULT_ENDPOINT_URL = "https://s3.eu-central-1.amazonaws.com/"
|
||||
AWS_DOMAIN = "amazonaws.com"
|
||||
DEFAULT_ENDPOINT_URL = f"https://s3.eu-central-1.{AWS_DOMAIN}/"
|
||||
|
||||
DATA_BACKUP_AGENT_LISTENERS: HassKey[list[Callable[[], None]]] = HassKey(
|
||||
f"{DOMAIN}.backup_agent_listeners"
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"domain": "s3",
|
||||
"name": "S3",
|
||||
"domain": "aws_s3",
|
||||
"name": "AWS S3",
|
||||
"codeowners": ["@tomasbedrich"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/s3",
|
||||
"documentation": "https://www.home-assistant.io/integrations/aws_s3",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aiobotocore"],
|
||||
+7
-7
@@ -9,19 +9,19 @@
|
||||
"endpoint_url": "Endpoint URL"
|
||||
},
|
||||
"data_description": {
|
||||
"access_key_id": "Access key ID to connect to S3 API",
|
||||
"secret_access_key": "Secret access key to connect to S3 API",
|
||||
"access_key_id": "Access key ID to connect to AWS S3 API",
|
||||
"secret_access_key": "Secret access key to connect to AWS S3 API",
|
||||
"bucket": "Bucket must already exist and be writable by the provided credentials.",
|
||||
"endpoint_url": "Endpoint URL provided to [Boto3 Session]({boto3_docs_url}). Region-specific [AWS S3 endpoints]({aws_s3_docs_url}) are available in their docs."
|
||||
},
|
||||
"title": "Add S3 bucket"
|
||||
"title": "Add AWS S3 bucket"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:component::s3::exceptions::cannot_connect::message%]",
|
||||
"invalid_bucket_name": "[%key:component::s3::exceptions::invalid_bucket_name::message%]",
|
||||
"invalid_credentials": "[%key:component::s3::exceptions::invalid_credentials::message%]",
|
||||
"invalid_endpoint_url": "Invalid endpoint URL"
|
||||
"cannot_connect": "[%key:component::aws_s3::exceptions::cannot_connect::message%]",
|
||||
"invalid_bucket_name": "[%key:component::aws_s3::exceptions::invalid_bucket_name::message%]",
|
||||
"invalid_credentials": "[%key:component::aws_s3::exceptions::invalid_credentials::message%]",
|
||||
"invalid_endpoint_url": "Invalid endpoint URL. Please make sure it's a valid AWS S3 endpoint URL."
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
@@ -12,5 +12,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/bluemaestro",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["bluemaestro-ble==0.4.0"]
|
||||
"requirements": ["bluemaestro-ble==0.4.1"]
|
||||
}
|
||||
|
||||
@@ -21,6 +21,6 @@
|
||||
"bluetooth-auto-recovery==1.5.1",
|
||||
"bluetooth-data-tools==1.28.1",
|
||||
"dbus-fast==2.43.0",
|
||||
"habluetooth==3.47.1"
|
||||
"habluetooth==3.48.2"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: FeedReaderConfigEntry)
|
||||
# if this is the last entry, remove the storage
|
||||
if len(entries) == 1:
|
||||
hass.data.pop(MY_KEY)
|
||||
return await hass.config_entries.async_unload_platforms(entry, Platform.EVENT)
|
||||
return await hass.config_entries.async_unload_platforms(entry, [Platform.EVENT])
|
||||
|
||||
|
||||
async def _async_update_listener(
|
||||
|
||||
@@ -144,6 +144,7 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature."""
|
||||
self.check_active_or_lock_mode()
|
||||
if kwargs.get(ATTR_HVAC_MODE) is HVACMode.OFF:
|
||||
await self.async_set_hkr_state("off")
|
||||
elif (target_temp := kwargs.get(ATTR_TEMPERATURE)) is not None:
|
||||
@@ -168,11 +169,7 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
"""Set new operation mode."""
|
||||
if self.data.holiday_active or self.data.summer_active:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="change_hvac_while_active_mode",
|
||||
)
|
||||
self.check_active_or_lock_mode()
|
||||
if self.hvac_mode is hvac_mode:
|
||||
LOGGER.debug(
|
||||
"%s is already in requested hvac mode %s", self.name, hvac_mode
|
||||
@@ -204,11 +201,7 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set preset mode."""
|
||||
if self.data.holiday_active or self.data.summer_active:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="change_preset_while_active_mode",
|
||||
)
|
||||
self.check_active_or_lock_mode()
|
||||
await self.async_set_hkr_state(PRESET_API_HKR_STATE_MAPPING[preset_mode])
|
||||
|
||||
@property
|
||||
@@ -230,3 +223,17 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
||||
attrs[ATTR_STATE_WINDOW_OPEN] = self.data.window_open
|
||||
|
||||
return attrs
|
||||
|
||||
def check_active_or_lock_mode(self) -> None:
|
||||
"""Check if in summer/vacation mode or lock enabled."""
|
||||
if self.data.holiday_active or self.data.summer_active:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="change_settings_while_active_mode",
|
||||
)
|
||||
|
||||
if self.data.lock:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="change_settings_while_lock_enabled",
|
||||
)
|
||||
|
||||
@@ -88,11 +88,11 @@
|
||||
"manual_switching_disabled": {
|
||||
"message": "Can't toggle switch while manual switching is disabled for the device."
|
||||
},
|
||||
"change_preset_while_active_mode": {
|
||||
"message": "Can't change preset while holiday or summer mode is active on the device."
|
||||
"change_settings_while_lock_enabled": {
|
||||
"message": "Can't change settings while manual access for telephone, app, or user interface is disabled on the device"
|
||||
},
|
||||
"change_hvac_while_active_mode": {
|
||||
"message": "Can't change HVAC mode while holiday or summer mode is active on the device."
|
||||
"change_settings_while_active_mode": {
|
||||
"message": "Can't change settings while holiday or summer mode is active on the device."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20250502.0"]
|
||||
"requirements": ["home-assistant-frontend==20250506.0"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/google",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["googleapiclient"],
|
||||
"requirements": ["gcal-sync==7.0.0", "oauth2client==4.1.3", "ical==9.1.0"]
|
||||
"requirements": ["gcal-sync==7.0.0", "oauth2client==4.1.3", "ical==9.2.0"]
|
||||
}
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import mimetypes
|
||||
from pathlib import Path
|
||||
|
||||
from google.genai import Client
|
||||
from google.genai.errors import APIError, ClientError
|
||||
from google.genai.types import File, FileState
|
||||
from requests.exceptions import Timeout
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -32,6 +34,8 @@ from .const import (
|
||||
CONF_CHAT_MODEL,
|
||||
CONF_PROMPT,
|
||||
DOMAIN,
|
||||
FILE_POLLING_INTERVAL_SECONDS,
|
||||
LOGGER,
|
||||
RECOMMENDED_CHAT_MODEL,
|
||||
TIMEOUT_MILLIS,
|
||||
)
|
||||
@@ -91,8 +95,40 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
)
|
||||
prompt_parts.append(uploaded_file)
|
||||
|
||||
async def wait_for_file_processing(uploaded_file: File) -> None:
|
||||
"""Wait for file processing to complete."""
|
||||
while True:
|
||||
uploaded_file = await client.aio.files.get(
|
||||
name=uploaded_file.name,
|
||||
config={"http_options": {"timeout": TIMEOUT_MILLIS}},
|
||||
)
|
||||
if uploaded_file.state not in (
|
||||
FileState.STATE_UNSPECIFIED,
|
||||
FileState.PROCESSING,
|
||||
):
|
||||
break
|
||||
LOGGER.debug(
|
||||
"Waiting for file `%s` to be processed, current state: %s",
|
||||
uploaded_file.name,
|
||||
uploaded_file.state,
|
||||
)
|
||||
await asyncio.sleep(FILE_POLLING_INTERVAL_SECONDS)
|
||||
|
||||
if uploaded_file.state == FileState.FAILED:
|
||||
raise HomeAssistantError(
|
||||
f"File `{uploaded_file.name}` processing failed, reason: {uploaded_file.error.message}"
|
||||
)
|
||||
|
||||
await hass.async_add_executor_job(append_files_to_prompt)
|
||||
|
||||
tasks = [
|
||||
asyncio.create_task(wait_for_file_processing(part))
|
||||
for part in prompt_parts
|
||||
if isinstance(part, File) and part.state != FileState.ACTIVE
|
||||
]
|
||||
async with asyncio.timeout(TIMEOUT_MILLIS / 1000):
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
try:
|
||||
response = await client.aio.models.generate_content(
|
||||
model=RECOMMENDED_CHAT_MODEL, contents=prompt_parts
|
||||
|
||||
@@ -26,3 +26,4 @@ CONF_USE_GOOGLE_SEARCH_TOOL = "enable_google_search_tool"
|
||||
RECOMMENDED_USE_GOOGLE_SEARCH_TOOL = False
|
||||
|
||||
TIMEOUT_MILLIS = 10000
|
||||
FILE_POLLING_INTERVAL_SECONDS = 0.05
|
||||
|
||||
@@ -109,6 +109,7 @@ class HassIOIngress(HomeAssistantView):
|
||||
delete = _handle
|
||||
patch = _handle
|
||||
options = _handle
|
||||
head = _handle
|
||||
|
||||
async def _handle_websocket(
|
||||
self, request: web.Request, token: str, path: str
|
||||
|
||||
@@ -9,10 +9,10 @@ from typing import Any
|
||||
|
||||
from homematicip.async_home import AsyncHome
|
||||
from homematicip.auth import Auth
|
||||
from homematicip.base.base_connection import HmipConnectionError
|
||||
from homematicip.base.enums import EventType
|
||||
from homematicip.connection.connection_context import ConnectionContextBuilder
|
||||
from homematicip.connection.rest_connection import RestConnection
|
||||
from homematicip.exceptions.connection_exceptions import HmipConnectionError
|
||||
|
||||
import homeassistant
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/homematicip_cloud",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["homematicip"],
|
||||
"requirements": ["homematicip==2.0.1"]
|
||||
"requirements": ["homematicip==2.0.1.1"]
|
||||
}
|
||||
|
||||
@@ -10,6 +10,11 @@ from aiohttp import web
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import http, sensor
|
||||
from homeassistant.components.button import (
|
||||
DOMAIN as BUTTON_DOMAIN,
|
||||
SERVICE_PRESS as SERVICE_PRESS_BUTTON,
|
||||
ButtonDeviceClass,
|
||||
)
|
||||
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
|
||||
from homeassistant.components.cover import (
|
||||
ATTR_POSITION,
|
||||
@@ -20,6 +25,7 @@ from homeassistant.components.cover import (
|
||||
CoverDeviceClass,
|
||||
)
|
||||
from homeassistant.components.http.data_validator import RequestDataValidator
|
||||
from homeassistant.components.input_button import DOMAIN as INPUT_BUTTON_DOMAIN
|
||||
from homeassistant.components.lock import (
|
||||
DOMAIN as LOCK_DOMAIN,
|
||||
SERVICE_LOCK,
|
||||
@@ -80,6 +86,7 @@ __all__ = [
|
||||
]
|
||||
|
||||
ONOFF_DEVICE_CLASSES = {
|
||||
ButtonDeviceClass,
|
||||
CoverDeviceClass,
|
||||
ValveDeviceClass,
|
||||
SwitchDeviceClass,
|
||||
@@ -103,7 +110,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
intent.INTENT_TURN_ON,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
description="Turns on/opens a device or entity. For locks, this performs a 'lock' action. Use for requests like 'turn on', 'activate', 'enable', or 'lock'.",
|
||||
description="Turns on/opens/presses a device or entity. For locks, this performs a 'lock' action. Use for requests like 'turn on', 'activate', 'enable', or 'lock'.",
|
||||
device_classes=ONOFF_DEVICE_CLASSES,
|
||||
),
|
||||
)
|
||||
@@ -168,6 +175,25 @@ class OnOffIntentHandler(intent.ServiceIntentHandler):
|
||||
"""Call service on entity with handling for special cases."""
|
||||
hass = intent_obj.hass
|
||||
|
||||
if state.domain in (BUTTON_DOMAIN, INPUT_BUTTON_DOMAIN):
|
||||
if service != SERVICE_TURN_ON:
|
||||
raise intent.IntentHandleError(
|
||||
f"Entity {state.entity_id} cannot be turned off"
|
||||
)
|
||||
|
||||
await self._run_then_background(
|
||||
hass.async_create_task(
|
||||
hass.services.async_call(
|
||||
state.domain,
|
||||
SERVICE_PRESS_BUTTON,
|
||||
{ATTR_ENTITY_ID: state.entity_id},
|
||||
context=intent_obj.context,
|
||||
blocking=True,
|
||||
)
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if state.domain == COVER_DOMAIN:
|
||||
# on = open
|
||||
# off = close
|
||||
|
||||
@@ -152,7 +152,10 @@ async def item_payload(item, get_thumbnail_url=None):
|
||||
_LOGGER.debug("Unknown media type received: %s", media_content_type)
|
||||
raise UnknownMediaType from err
|
||||
|
||||
thumbnail = item.get("thumbnail")
|
||||
if "art" in item:
|
||||
thumbnail = item["art"].get("poster", item.get("thumbnail"))
|
||||
else:
|
||||
thumbnail = item.get("thumbnail")
|
||||
if thumbnail is not None and get_thumbnail_url is not None:
|
||||
thumbnail = await get_thumbnail_url(
|
||||
media_content_type, media_content_id, thumbnail_url=thumbnail
|
||||
@@ -237,14 +240,16 @@ async def get_media_info(media_library, search_id, search_type):
|
||||
title = None
|
||||
media = None
|
||||
|
||||
properties = ["thumbnail"]
|
||||
properties = ["thumbnail", "art"]
|
||||
if search_type == MediaType.ALBUM:
|
||||
if search_id:
|
||||
album = await media_library.get_album_details(
|
||||
album_id=int(search_id), properties=properties
|
||||
)
|
||||
thumbnail = media_library.thumbnail_url(
|
||||
album["albumdetails"].get("thumbnail")
|
||||
album["albumdetails"]["art"].get(
|
||||
"poster", album["albumdetails"].get("thumbnail")
|
||||
)
|
||||
)
|
||||
title = album["albumdetails"]["label"]
|
||||
media = await media_library.get_songs(
|
||||
@@ -256,6 +261,7 @@ async def get_media_info(media_library, search_id, search_type):
|
||||
"album",
|
||||
"thumbnail",
|
||||
"track",
|
||||
"art",
|
||||
],
|
||||
)
|
||||
media = media.get("songs")
|
||||
@@ -274,7 +280,9 @@ async def get_media_info(media_library, search_id, search_type):
|
||||
artist_id=int(search_id), properties=properties
|
||||
)
|
||||
thumbnail = media_library.thumbnail_url(
|
||||
artist["artistdetails"].get("thumbnail")
|
||||
artist["artistdetails"]["art"].get(
|
||||
"poster", artist["artistdetails"].get("thumbnail")
|
||||
)
|
||||
)
|
||||
title = artist["artistdetails"]["label"]
|
||||
else:
|
||||
@@ -293,9 +301,10 @@ async def get_media_info(media_library, search_id, search_type):
|
||||
movie_id=int(search_id), properties=properties
|
||||
)
|
||||
thumbnail = media_library.thumbnail_url(
|
||||
movie["moviedetails"].get("thumbnail")
|
||||
movie["moviedetails"]["art"].get(
|
||||
"poster", movie["moviedetails"].get("thumbnail")
|
||||
)
|
||||
)
|
||||
title = movie["moviedetails"]["label"]
|
||||
else:
|
||||
media = await media_library.get_movies(properties)
|
||||
media = media.get("movies")
|
||||
@@ -305,14 +314,16 @@ async def get_media_info(media_library, search_id, search_type):
|
||||
if search_id:
|
||||
media = await media_library.get_seasons(
|
||||
tv_show_id=int(search_id),
|
||||
properties=["thumbnail", "season", "tvshowid"],
|
||||
properties=["thumbnail", "season", "tvshowid", "art"],
|
||||
)
|
||||
media = media.get("seasons")
|
||||
tvshow = await media_library.get_tv_show_details(
|
||||
tv_show_id=int(search_id), properties=properties
|
||||
)
|
||||
thumbnail = media_library.thumbnail_url(
|
||||
tvshow["tvshowdetails"].get("thumbnail")
|
||||
tvshow["tvshowdetails"]["art"].get(
|
||||
"poster", tvshow["tvshowdetails"].get("thumbnail")
|
||||
)
|
||||
)
|
||||
title = tvshow["tvshowdetails"]["label"]
|
||||
else:
|
||||
@@ -325,7 +336,7 @@ async def get_media_info(media_library, search_id, search_type):
|
||||
media = await media_library.get_episodes(
|
||||
tv_show_id=int(tv_show_id),
|
||||
season_id=int(season_id),
|
||||
properties=["thumbnail", "tvshowid", "seasonid"],
|
||||
properties=["thumbnail", "tvshowid", "seasonid", "art"],
|
||||
)
|
||||
media = media.get("episodes")
|
||||
if media:
|
||||
@@ -333,7 +344,9 @@ async def get_media_info(media_library, search_id, search_type):
|
||||
season_id=int(media[0]["seasonid"]), properties=properties
|
||||
)
|
||||
thumbnail = media_library.thumbnail_url(
|
||||
season["seasondetails"].get("thumbnail")
|
||||
season["seasondetails"]["art"].get(
|
||||
"poster", season["seasondetails"].get("thumbnail")
|
||||
)
|
||||
)
|
||||
title = season["seasondetails"]["label"]
|
||||
|
||||
@@ -343,6 +356,7 @@ async def get_media_info(media_library, search_id, search_type):
|
||||
properties=["thumbnail", "channeltype", "channel", "broadcastnow"],
|
||||
)
|
||||
media = media.get("channels")
|
||||
|
||||
title = "Channels"
|
||||
|
||||
return thumbnail, title, media
|
||||
|
||||
@@ -23,7 +23,7 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import CONF_USE_BLUETOOTH, DOMAIN
|
||||
from .coordinator import (
|
||||
@@ -57,7 +57,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) -
|
||||
assert entry.unique_id
|
||||
serial = entry.unique_id
|
||||
|
||||
client = async_create_clientsession(hass)
|
||||
client = async_get_clientsession(hass)
|
||||
cloud_client = LaMarzoccoCloudClient(
|
||||
username=entry.data[CONF_USERNAME],
|
||||
password=entry.data[CONF_PASSWORD],
|
||||
|
||||
@@ -33,7 +33,7 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.selector import (
|
||||
SelectOptionDict,
|
||||
SelectSelector,
|
||||
@@ -83,7 +83,7 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
**user_input,
|
||||
}
|
||||
|
||||
self._client = async_create_clientsession(self.hass)
|
||||
self._client = async_get_clientsession(self.hass)
|
||||
cloud_client = LaMarzoccoCloudClient(
|
||||
username=data[CONF_USERNAME],
|
||||
password=data[CONF_PASSWORD],
|
||||
|
||||
@@ -37,5 +37,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pylamarzocco"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pylamarzocco==2.0.0b7"]
|
||||
"requirements": ["pylamarzocco==2.0.0"]
|
||||
}
|
||||
|
||||
@@ -89,20 +89,27 @@ class LocalCalendarEntity(CalendarEntity):
|
||||
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
|
||||
) -> list[CalendarEvent]:
|
||||
"""Get all events in a specific time frame."""
|
||||
events = self._calendar.timeline_tz(start_date.tzinfo).overlapping(
|
||||
start_date,
|
||||
end_date,
|
||||
)
|
||||
return [_get_calendar_event(event) for event in events]
|
||||
|
||||
def events_in_range() -> list[CalendarEvent]:
|
||||
events = self._calendar.timeline_tz(start_date.tzinfo).overlapping(
|
||||
start_date,
|
||||
end_date,
|
||||
)
|
||||
return [_get_calendar_event(event) for event in events]
|
||||
|
||||
return await self.hass.async_add_executor_job(events_in_range)
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update entity state with the next upcoming event."""
|
||||
now = dt_util.now()
|
||||
events = self._calendar.timeline_tz(now.tzinfo).active_after(now)
|
||||
if event := next(events, None):
|
||||
self._event = _get_calendar_event(event)
|
||||
else:
|
||||
self._event = None
|
||||
|
||||
def next_event() -> CalendarEvent | None:
|
||||
now = dt_util.now()
|
||||
events = self._calendar.timeline_tz(now.tzinfo).active_after(now)
|
||||
if event := next(events, None):
|
||||
return _get_calendar_event(event)
|
||||
return None
|
||||
|
||||
self._event = await self.hass.async_add_executor_job(next_event)
|
||||
|
||||
async def _async_store(self) -> None:
|
||||
"""Persist the calendar to disk."""
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["ical"],
|
||||
"requirements": ["ical==9.1.0"]
|
||||
"requirements": ["ical==9.2.0"]
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_todo",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["ical==9.1.0"]
|
||||
"requirements": ["ical==9.2.0"]
|
||||
}
|
||||
|
||||
@@ -93,7 +93,6 @@ async def async_setup_intents(hass: HomeAssistant) -> None:
|
||||
DOMAIN,
|
||||
SERVICE_VOLUME_SET,
|
||||
required_domains={DOMAIN},
|
||||
required_states={MediaPlayerState.PLAYING},
|
||||
required_features=MediaPlayerEntityFeature.VOLUME_SET,
|
||||
required_slots={
|
||||
ATTR_MEDIA_VOLUME_LEVEL: intent.IntentSlotInfo(
|
||||
@@ -159,7 +158,6 @@ class MediaUnpauseHandler(intent.ServiceIntentHandler):
|
||||
DOMAIN,
|
||||
SERVICE_MEDIA_PLAY,
|
||||
required_domains={DOMAIN},
|
||||
required_states={MediaPlayerState.PAUSED},
|
||||
description="Resumes a media player",
|
||||
platforms={DOMAIN},
|
||||
device_classes={MediaPlayerDeviceClass},
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pymiele"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pymiele==0.4.1"],
|
||||
"requirements": ["pymiele==0.4.3"],
|
||||
"single_config_entry": true,
|
||||
"zeroconf": ["_mieleathome._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -144,7 +144,6 @@ SENSOR_TYPES: Final[tuple[MieleSensorDefinition, ...]] = (
|
||||
MieleAppliance.STEAM_OVEN,
|
||||
MieleAppliance.MICROWAVE,
|
||||
MieleAppliance.COFFEE_SYSTEM,
|
||||
MieleAppliance.ROBOT_VACUUM_CLEANER,
|
||||
MieleAppliance.WASHER_DRYER,
|
||||
MieleAppliance.STEAM_OVEN_COMBI,
|
||||
MieleAppliance.STEAM_OVEN_MICRO,
|
||||
|
||||
@@ -465,7 +465,7 @@ class PlatformField:
|
||||
required: bool
|
||||
validator: Callable[..., Any]
|
||||
error: str | None = None
|
||||
default: str | int | bool | vol.Undefined = vol.UNDEFINED
|
||||
default: str | int | bool | None | vol.Undefined = vol.UNDEFINED
|
||||
is_schema_default: bool = False
|
||||
exclude_from_reconfig: bool = False
|
||||
conditions: tuple[dict[str, Any], ...] | None = None
|
||||
@@ -498,8 +498,7 @@ def validate_light_platform_config(user_data: dict[str, Any]) -> dict[str, str]:
|
||||
if user_data.get(CONF_MIN_KELVIN, DEFAULT_MIN_KELVIN) >= user_data.get(
|
||||
CONF_MAX_KELVIN, DEFAULT_MAX_KELVIN
|
||||
):
|
||||
errors[CONF_MAX_KELVIN] = "max_below_min_kelvin"
|
||||
errors[CONF_MIN_KELVIN] = "max_below_min_kelvin"
|
||||
errors["advanced_settings"] = "max_below_min_kelvin"
|
||||
return errors
|
||||
|
||||
|
||||
@@ -515,6 +514,7 @@ COMMON_ENTITY_FIELDS = {
|
||||
required=False,
|
||||
validator=str,
|
||||
exclude_from_reconfig=True,
|
||||
default=None,
|
||||
),
|
||||
CONF_ENTITY_PICTURE: PlatformField(
|
||||
selector=TEXT_SELECTOR, required=False, validator=cv.url, error="invalid_url"
|
||||
@@ -1150,7 +1150,7 @@ ENTITY_CONFIG_VALIDATOR: dict[
|
||||
}
|
||||
|
||||
MQTT_DEVICE_PLATFORM_FIELDS = {
|
||||
ATTR_NAME: PlatformField(selector=TEXT_SELECTOR, required=False, validator=str),
|
||||
ATTR_NAME: PlatformField(selector=TEXT_SELECTOR, required=True, validator=str),
|
||||
ATTR_SW_VERSION: PlatformField(
|
||||
selector=TEXT_SELECTOR, required=False, validator=str
|
||||
),
|
||||
@@ -1275,7 +1275,10 @@ def validate_user_input(
|
||||
try:
|
||||
validator(value)
|
||||
except (ValueError, vol.Error, vol.Invalid):
|
||||
errors[field] = data_schema_fields[field].error or "invalid_input"
|
||||
data_schema_field = data_schema_fields[field]
|
||||
errors[data_schema_field.section or field] = (
|
||||
data_schema_field.error or "invalid_input"
|
||||
)
|
||||
|
||||
if config_validator is not None:
|
||||
if TYPE_CHECKING:
|
||||
@@ -1324,7 +1327,10 @@ def data_schema_from_fields(
|
||||
vol.Required(field_name, default=field_details.default)
|
||||
if field_details.required
|
||||
else vol.Optional(
|
||||
field_name, default=field_details.default
|
||||
field_name,
|
||||
default=field_details.default
|
||||
if field_details.default is not None
|
||||
else vol.UNDEFINED,
|
||||
): field_details.selector(component_data_with_user_input) # type: ignore[operator]
|
||||
if field_details.custom_filtering
|
||||
else field_details.selector
|
||||
@@ -1375,12 +1381,17 @@ def data_schema_from_fields(
|
||||
@callback
|
||||
def subentry_schema_default_data_from_fields(
|
||||
data_schema_fields: dict[str, PlatformField],
|
||||
component_data: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
"""Generate custom data schema from platform fields or device data."""
|
||||
return {
|
||||
key: field.default
|
||||
for key, field in data_schema_fields.items()
|
||||
if field.is_schema_default
|
||||
if _check_conditions(field, component_data)
|
||||
and (
|
||||
field.is_schema_default
|
||||
or (field.default is not vol.UNDEFINED and key not in component_data)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2206,7 +2217,10 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
|
||||
for component_data in self._subentry_data["components"].values():
|
||||
platform = component_data[CONF_PLATFORM]
|
||||
subentry_default_data = subentry_schema_default_data_from_fields(
|
||||
PLATFORM_ENTITY_FIELDS[platform]
|
||||
COMMON_ENTITY_FIELDS
|
||||
| PLATFORM_ENTITY_FIELDS[platform]
|
||||
| PLATFORM_MQTT_FIELDS[platform],
|
||||
component_data,
|
||||
)
|
||||
component_data.update(subentry_default_data)
|
||||
|
||||
|
||||
@@ -113,14 +113,16 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
|
||||
_LOGGER.error("Error getting accounts: %s", err)
|
||||
raise
|
||||
for account in accounts:
|
||||
id_prefix = "_".join(
|
||||
id_prefix = (
|
||||
(
|
||||
self.api.utility.subdomain(),
|
||||
account.meter_type.name.lower(),
|
||||
# Some utilities like AEP have "-" in their account id.
|
||||
# Replace it with "_" to avoid "Invalid statistic_id"
|
||||
account.utility_account_id.replace("-", "_").lower(),
|
||||
f"{self.api.utility.subdomain()}_{account.meter_type.name}_"
|
||||
f"{account.utility_account_id}"
|
||||
)
|
||||
# Some utilities like AEP have "-" in their account id.
|
||||
# Other utilities like ngny-gas have "-" in their subdomain.
|
||||
# Replace it with "_" to avoid "Invalid statistic_id"
|
||||
.replace("-", "_")
|
||||
.lower()
|
||||
)
|
||||
cost_statistic_id = f"{DOMAIN}:{id_prefix}_energy_cost"
|
||||
compensation_statistic_id = f"{DOMAIN}:{id_prefix}_energy_compensation"
|
||||
@@ -190,7 +192,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
|
||||
return_sum = 0.0
|
||||
last_stats_time = None
|
||||
else:
|
||||
await self._async_maybe_migrate_statistics(
|
||||
migrated = await self._async_maybe_migrate_statistics(
|
||||
account.utility_account_id,
|
||||
{
|
||||
cost_statistic_id: compensation_statistic_id,
|
||||
@@ -203,6 +205,13 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
|
||||
return_statistic_id: return_metadata,
|
||||
},
|
||||
)
|
||||
if migrated:
|
||||
# Skip update to avoid working on old data since the migration is done
|
||||
# asynchronously. Update the statistics in the next refresh in 12h.
|
||||
_LOGGER.debug(
|
||||
"Statistics migration completed. Skipping update for now"
|
||||
)
|
||||
continue
|
||||
cost_reads = await self._async_get_cost_reads(
|
||||
account,
|
||||
self.api.utility.timezone(),
|
||||
@@ -326,7 +335,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
|
||||
utility_account_id: str,
|
||||
migration_map: dict[str, str],
|
||||
metadata_map: dict[str, StatisticMetaData],
|
||||
) -> None:
|
||||
) -> bool:
|
||||
"""Perform one-time statistics migration based on the provided map.
|
||||
|
||||
Splits negative values from source IDs into target IDs.
|
||||
@@ -339,7 +348,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
|
||||
|
||||
"""
|
||||
if not migration_map:
|
||||
return
|
||||
return False
|
||||
|
||||
need_migration_source_ids = set()
|
||||
for source_id, target_id in migration_map.items():
|
||||
@@ -354,7 +363,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
|
||||
if not last_target_stat:
|
||||
need_migration_source_ids.add(source_id)
|
||||
if not need_migration_source_ids:
|
||||
return
|
||||
return False
|
||||
|
||||
_LOGGER.info("Starting one-time migration for: %s", need_migration_source_ids)
|
||||
|
||||
@@ -416,7 +425,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
|
||||
|
||||
if not need_migration_source_ids:
|
||||
_LOGGER.debug("No migration needed")
|
||||
return
|
||||
return False
|
||||
|
||||
for stat_id, stats in processed_stats.items():
|
||||
_LOGGER.debug("Applying %d migrated stats for %s", len(stats), stat_id)
|
||||
@@ -434,7 +443,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
|
||||
"energy_settings": "/config/energy",
|
||||
"target_ids": "\n".join(
|
||||
{
|
||||
v
|
||||
str(metadata_map[v]["name"])
|
||||
for k, v in migration_map.items()
|
||||
if k in need_migration_source_ids
|
||||
}
|
||||
@@ -442,6 +451,8 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
|
||||
},
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
async def _async_get_cost_reads(
|
||||
self, account: Account, time_zone_str: str, start_time: float | None = None
|
||||
) -> list[CostRead]:
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"issues": {
|
||||
"return_to_grid_migration": {
|
||||
"title": "Return to grid statistics for account: {utility_account_id}",
|
||||
"description": "We found negative values in your existing consumption statistics, likely because you have solar. We split those in separate return statistics for a better experience in the Energy dashboard.\n\nPlease visit the [Energy configuration page]({energy_settings}) to add the following statistics in the **Return to grid** section:\n\n{target_ids}"
|
||||
"description": "We found negative values in your existing consumption statistics, likely because you have solar. We split those in separate return statistics for a better experience in the Energy dashboard.\n\nPlease visit the [Energy configuration page]({energy_settings}) to add the following statistics in the **Return to grid** section:\n\n{target_ids}\n\nOnce you have added them, ignore this issue."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,10 +56,15 @@ class PicoProvider(Provider):
|
||||
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmpf:
|
||||
fname = tmpf.name
|
||||
|
||||
cmd = ["pico2wave", "--wave", fname, "-l", language, "--", message]
|
||||
subprocess.call(cmd)
|
||||
cmd = ["pico2wave", "--wave", fname, "-l", language]
|
||||
result = subprocess.run(cmd, text=True, input=message, check=False)
|
||||
data = None
|
||||
try:
|
||||
if result.returncode != 0:
|
||||
_LOGGER.error(
|
||||
"Error running pico2wave, return code: %s", result.returncode
|
||||
)
|
||||
return (None, None)
|
||||
with open(fname, "rb") as voice:
|
||||
data = voice.read()
|
||||
except OSError:
|
||||
|
||||
@@ -29,6 +29,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: RehlkoConfigEntry) -> bo
|
||||
"""Set up Rehlko from a config entry."""
|
||||
websession = async_get_clientsession(hass)
|
||||
rehlko = AioKem(session=websession)
|
||||
# If requests take more than 20 seconds; timeout and let the setup retry.
|
||||
rehlko.set_timeout(20)
|
||||
|
||||
async def async_refresh_token_update(refresh_token: str) -> None:
|
||||
"""Handle refresh token update."""
|
||||
@@ -40,7 +42,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: RehlkoConfigEntry) -> bo
|
||||
)
|
||||
|
||||
rehlko.set_refresh_token_callback(async_refresh_token_update)
|
||||
rehlko.set_retry_policy(retry_count=3, retry_delays=[5, 10, 20])
|
||||
|
||||
try:
|
||||
await rehlko.authenticate(
|
||||
@@ -48,6 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: RehlkoConfigEntry) -> bo
|
||||
entry.data[CONF_PASSWORD],
|
||||
entry.data.get(CONF_REFRESH_TOKEN),
|
||||
)
|
||||
homes = await rehlko.get_homes()
|
||||
except AuthenticationError as ex:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
@@ -60,7 +62,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: RehlkoConfigEntry) -> bo
|
||||
translation_key="cannot_connect",
|
||||
) from ex
|
||||
coordinators: dict[int, RehlkoUpdateCoordinator] = {}
|
||||
homes = await rehlko.get_homes()
|
||||
|
||||
entry.runtime_data = RehlkoRuntimeData(
|
||||
coordinators=coordinators,
|
||||
@@ -86,6 +87,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: RehlkoConfigEntry) -> bo
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
coordinators[device_id] = coordinator
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
# Retrys enabled after successful connection to prevent blocking startup
|
||||
rehlko.set_retry_policy(retry_count=3, retry_delays=[5, 10, 20])
|
||||
# Rehlko service can be slow to respond, increase timeout for polls.
|
||||
rehlko.set_timeout(100)
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aiokem"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aiokem==0.5.9"]
|
||||
"requirements": ["aiokem==0.5.10"]
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ async def async_setup_entry(
|
||||
"""Set up the remote calendar platform."""
|
||||
coordinator = entry.runtime_data
|
||||
entity = RemoteCalendarEntity(coordinator, entry)
|
||||
async_add_entities([entity])
|
||||
async_add_entities([entity], True)
|
||||
|
||||
|
||||
class RemoteCalendarEntity(
|
||||
@@ -48,25 +48,46 @@ class RemoteCalendarEntity(
|
||||
super().__init__(coordinator)
|
||||
self._attr_name = entry.data[CONF_CALENDAR_NAME]
|
||||
self._attr_unique_id = entry.entry_id
|
||||
self._event: CalendarEvent | None = None
|
||||
|
||||
@property
|
||||
def event(self) -> CalendarEvent | None:
|
||||
"""Return the next upcoming event."""
|
||||
now = dt_util.now()
|
||||
events = self.coordinator.data.timeline_tz(now.tzinfo).active_after(now)
|
||||
if event := next(events, None):
|
||||
return _get_calendar_event(event)
|
||||
return None
|
||||
return self._event
|
||||
|
||||
async def async_get_events(
|
||||
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
|
||||
) -> list[CalendarEvent]:
|
||||
"""Get all events in a specific time frame."""
|
||||
events = self.coordinator.data.timeline_tz(start_date.tzinfo).overlapping(
|
||||
start_date,
|
||||
end_date,
|
||||
)
|
||||
return [_get_calendar_event(event) for event in events]
|
||||
|
||||
def events_in_range() -> list[CalendarEvent]:
|
||||
"""Return all events in the given time range."""
|
||||
events = self.coordinator.data.timeline_tz(start_date.tzinfo).overlapping(
|
||||
start_date,
|
||||
end_date,
|
||||
)
|
||||
return [_get_calendar_event(event) for event in events]
|
||||
|
||||
return await self.hass.async_add_executor_job(events_in_range)
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Refresh the timeline.
|
||||
|
||||
This is called when the coordinator updates. Creating the timeline may
|
||||
require walking through the entire calendar and handling recurring
|
||||
events, so it is done as a separate task without blocking the event loop.
|
||||
"""
|
||||
await super().async_update()
|
||||
|
||||
def next_timeline_event() -> CalendarEvent | None:
|
||||
"""Return the next active event."""
|
||||
now = dt_util.now()
|
||||
events = self.coordinator.data.timeline_tz(now.tzinfo).active_after(now)
|
||||
if event := next(events, None):
|
||||
return _get_calendar_event(event)
|
||||
return None
|
||||
|
||||
self._event = await self.hass.async_add_executor_job(next_timeline_event)
|
||||
|
||||
|
||||
def _get_calendar_event(event: Event) -> CalendarEvent:
|
||||
|
||||
@@ -5,8 +5,6 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
from httpx import HTTPError, InvalidURL
|
||||
from ical.calendar_stream import IcsCalendarStream
|
||||
from ical.exceptions import CalendarParseError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
@@ -14,6 +12,7 @@ from homeassistant.const import CONF_URL
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
|
||||
from .const import CONF_CALENDAR_NAME, DOMAIN
|
||||
from .ics import InvalidIcsException, parse_calendar
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -64,15 +63,9 @@ class RemoteCalendarConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
_LOGGER.debug("An error occurred: %s", err)
|
||||
else:
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
IcsCalendarStream.calendar_from_ics, res.text
|
||||
)
|
||||
except CalendarParseError as err:
|
||||
await parse_calendar(self.hass, res.text)
|
||||
except InvalidIcsException:
|
||||
errors["base"] = "invalid_ics_file"
|
||||
_LOGGER.error("Error reading the calendar information: %s", err.message)
|
||||
_LOGGER.debug(
|
||||
"Additional calendar error detail: %s", str(err.detailed_error)
|
||||
)
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_CALENDAR_NAME], data=user_input
|
||||
|
||||
@@ -5,8 +5,6 @@ import logging
|
||||
|
||||
from httpx import HTTPError, InvalidURL
|
||||
from ical.calendar import Calendar
|
||||
from ical.calendar_stream import IcsCalendarStream
|
||||
from ical.exceptions import CalendarParseError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_URL
|
||||
@@ -15,6 +13,7 @@ from homeassistant.helpers.httpx_client import get_async_client
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
from .ics import InvalidIcsException, parse_calendar
|
||||
|
||||
type RemoteCalendarConfigEntry = ConfigEntry[RemoteCalendarDataUpdateCoordinator]
|
||||
|
||||
@@ -56,14 +55,9 @@ class RemoteCalendarDataUpdateCoordinator(DataUpdateCoordinator[Calendar]):
|
||||
translation_placeholders={"err": str(err)},
|
||||
) from err
|
||||
try:
|
||||
# calendar_from_ics will dynamically load packages
|
||||
# the first time it is called, so we need to do it
|
||||
# in a separate thread to avoid blocking the event loop
|
||||
self.ics = res.text
|
||||
return await self.hass.async_add_executor_job(
|
||||
IcsCalendarStream.calendar_from_ics, self.ics
|
||||
)
|
||||
except CalendarParseError as err:
|
||||
return await parse_calendar(self.hass, res.text)
|
||||
except InvalidIcsException as err:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="unable_to_parse",
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Module for parsing ICS content.
|
||||
|
||||
This module exists to fix known issues where calendar providers return calendars
|
||||
that do not follow rfcc5545. This module will attempt to fix the calendar and return
|
||||
a valid calendar object.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from ical.calendar import Calendar
|
||||
from ical.calendar_stream import IcsCalendarStream
|
||||
from ical.compat import enable_compat_mode
|
||||
from ical.exceptions import CalendarParseError
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class InvalidIcsException(Exception):
|
||||
"""Exception to indicate that the ICS content is invalid."""
|
||||
|
||||
|
||||
def _compat_calendar_from_ics(ics: str) -> Calendar:
|
||||
"""Parse the ICS content and return a Calendar object.
|
||||
|
||||
This function is called in a separate thread to avoid blocking the event
|
||||
loop while loading packages or parsing the ICS content for large calendars.
|
||||
|
||||
It uses the `enable_compat_mode` context manager to fix known issues with
|
||||
calendar providers that return invalid calendars.
|
||||
"""
|
||||
with enable_compat_mode(ics) as compat_ics:
|
||||
return IcsCalendarStream.calendar_from_ics(compat_ics)
|
||||
|
||||
|
||||
async def parse_calendar(hass: HomeAssistant, ics: str) -> Calendar:
|
||||
"""Parse the ICS content and return a Calendar object."""
|
||||
try:
|
||||
return await hass.async_add_executor_job(_compat_calendar_from_ics, ics)
|
||||
except CalendarParseError as err:
|
||||
_LOGGER.error("Error parsing calendar information: %s", err.message)
|
||||
_LOGGER.debug("Additional calendar error detail: %s", str(err.detailed_error))
|
||||
raise InvalidIcsException(err.message) from err
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["ical"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["ical==9.1.0"]
|
||||
"requirements": ["ical==9.2.0"]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["renault_api"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["renault-api==0.3.0"]
|
||||
"requirements": ["renault-api==0.3.1"]
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"loggers": ["roborock"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": [
|
||||
"python-roborock==2.16.1",
|
||||
"vacuum-map-parser-roborock==0.1.2"
|
||||
"python-roborock==2.18.2",
|
||||
"vacuum-map-parser-roborock==0.1.4"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ class TTSMediaSource(MediaSource):
|
||||
raise BrowseError("Unknown provider")
|
||||
|
||||
if isinstance(engine_instance, TextToSpeechEntity):
|
||||
engine_domain = engine_instance.platform.domain
|
||||
engine_domain = engine_instance.platform.platform_name
|
||||
else:
|
||||
engine_domain = engine
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["uiprotect", "unifi_discovery"],
|
||||
"requirements": ["uiprotect==7.5.5", "unifi-discovery==1.2.0"],
|
||||
"requirements": ["uiprotect==7.6.0", "unifi-discovery==1.2.0"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Ubiquiti Networks",
|
||||
|
||||
@@ -51,9 +51,9 @@ if TYPE_CHECKING:
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_PIPELINE_TIMEOUT_SEC: Final = 30
|
||||
_HANGUP_SEC: Final = 0.5
|
||||
_ANNOUNCEMENT_BEFORE_DELAY: Final = 0.5
|
||||
_ANNOUNCEMENT_AFTER_DELAY: Final = 1.0
|
||||
_ANNOUNCEMENT_HANGUP_SEC: Final = 0.5
|
||||
_ANNOUNCEMENT_RING_TIMEOUT: Final = 30
|
||||
|
||||
|
||||
@@ -132,9 +132,10 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol
|
||||
self._processing_tone_done = asyncio.Event()
|
||||
|
||||
self._announcement: AssistSatelliteAnnouncement | None = None
|
||||
self._announcement_future: asyncio.Future[Any] = asyncio.Future()
|
||||
self._announcment_start_time: float = 0.0
|
||||
self._check_announcement_ended_task: asyncio.Task | None = None
|
||||
self._check_announcement_pickup_task: asyncio.Task | None = None
|
||||
self._check_hangup_task: asyncio.Task | None = None
|
||||
self._call_end_future: asyncio.Future[Any] = asyncio.Future()
|
||||
self._last_chunk_time: float | None = None
|
||||
self._rtp_port: int | None = None
|
||||
self._run_pipeline_after_announce: bool = False
|
||||
@@ -233,7 +234,7 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol
|
||||
translation_key="non_tts_announcement",
|
||||
)
|
||||
|
||||
self._announcement_future = asyncio.Future()
|
||||
self._call_end_future = asyncio.Future()
|
||||
self._run_pipeline_after_announce = run_pipeline_after
|
||||
|
||||
if self._rtp_port is None:
|
||||
@@ -274,53 +275,77 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol
|
||||
rtp_port=self._rtp_port,
|
||||
)
|
||||
|
||||
# Check if caller hung up or didn't pick up
|
||||
self._check_announcement_ended_task = (
|
||||
# Check if caller didn't pick up
|
||||
self._check_announcement_pickup_task = (
|
||||
self.config_entry.async_create_background_task(
|
||||
self.hass,
|
||||
self._check_announcement_ended(),
|
||||
"voip_announcement_ended",
|
||||
self._check_announcement_pickup(),
|
||||
"voip_announcement_pickup",
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
await self._announcement_future
|
||||
await self._call_end_future
|
||||
except TimeoutError:
|
||||
# Stop ringing
|
||||
_LOGGER.debug("Caller did not pick up in time")
|
||||
sip_protocol.cancel_call(call_info)
|
||||
raise
|
||||
|
||||
async def _check_announcement_ended(self) -> None:
|
||||
async def _check_announcement_pickup(self) -> None:
|
||||
"""Continuously checks if an audio chunk was received within a time limit.
|
||||
|
||||
If not, the caller is presumed to have hung up and the announcement is ended.
|
||||
If not, the caller is presumed to have not picked up the phone and the announcement is ended.
|
||||
"""
|
||||
while self._announcement is not None:
|
||||
while True:
|
||||
current_time = time.monotonic()
|
||||
if (self._last_chunk_time is None) and (
|
||||
(current_time - self._announcment_start_time)
|
||||
> _ANNOUNCEMENT_RING_TIMEOUT
|
||||
):
|
||||
# Ring timeout
|
||||
_LOGGER.debug("Ring timeout")
|
||||
self._announcement = None
|
||||
self._check_announcement_ended_task = None
|
||||
self._announcement_future.set_exception(
|
||||
self._check_announcement_pickup_task = None
|
||||
self._call_end_future.set_exception(
|
||||
TimeoutError("User did not pick up in time")
|
||||
)
|
||||
_LOGGER.debug("Timed out waiting for the user to pick up the phone")
|
||||
break
|
||||
|
||||
if (self._last_chunk_time is not None) and (
|
||||
(current_time - self._last_chunk_time) > _ANNOUNCEMENT_HANGUP_SEC
|
||||
):
|
||||
# Caller hung up
|
||||
self._announcement = None
|
||||
self._announcement_future.set_result(None)
|
||||
self._check_announcement_ended_task = None
|
||||
_LOGGER.debug("Announcement ended")
|
||||
if self._last_chunk_time is not None:
|
||||
_LOGGER.debug("Picked up the phone")
|
||||
self._check_announcement_pickup_task = None
|
||||
break
|
||||
|
||||
await asyncio.sleep(_ANNOUNCEMENT_HANGUP_SEC / 2)
|
||||
await asyncio.sleep(_HANGUP_SEC / 2)
|
||||
|
||||
async def _check_hangup(self) -> None:
|
||||
"""Continuously checks if an audio chunk was received within a time limit.
|
||||
|
||||
If not, the caller is presumed to have hung up and the call is ended.
|
||||
"""
|
||||
try:
|
||||
while True:
|
||||
current_time = time.monotonic()
|
||||
if (self._last_chunk_time is not None) and (
|
||||
(current_time - self._last_chunk_time) > _HANGUP_SEC
|
||||
):
|
||||
# Caller hung up
|
||||
_LOGGER.debug("Hang up")
|
||||
self._announcement = None
|
||||
if self._run_pipeline_task is not None:
|
||||
_LOGGER.debug("Cancelling running pipeline")
|
||||
self._run_pipeline_task.cancel()
|
||||
self._call_end_future.set_result(None)
|
||||
self.disconnect()
|
||||
break
|
||||
|
||||
await asyncio.sleep(_HANGUP_SEC / 2)
|
||||
except asyncio.CancelledError:
|
||||
# Don't swallow cancellation
|
||||
if (current_task := asyncio.current_task()) and current_task.cancelling():
|
||||
raise
|
||||
_LOGGER.debug("Check hangup cancelled")
|
||||
|
||||
async def async_start_conversation(
|
||||
self, start_announcement: AssistSatelliteAnnouncement
|
||||
@@ -332,6 +357,24 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol
|
||||
# VoIP
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def disconnect(self):
|
||||
"""Server disconnected."""
|
||||
super().disconnect()
|
||||
if self._check_hangup_task is not None:
|
||||
self._check_hangup_task.cancel()
|
||||
self._check_hangup_task = None
|
||||
|
||||
def connection_made(self, transport):
|
||||
"""Server is ready."""
|
||||
super().connection_made(transport)
|
||||
self._last_chunk_time = time.monotonic()
|
||||
# Check if caller hung up
|
||||
self._check_hangup_task = self.config_entry.async_create_background_task(
|
||||
self.hass,
|
||||
self._check_hangup(),
|
||||
"voip_hangup",
|
||||
)
|
||||
|
||||
def on_chunk(self, audio_bytes: bytes) -> None:
|
||||
"""Handle raw audio chunk."""
|
||||
self._last_chunk_time = time.monotonic()
|
||||
@@ -368,13 +411,22 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol
|
||||
self.voip_device.set_is_active(True)
|
||||
|
||||
async def stt_stream():
|
||||
retry: bool = True
|
||||
while True:
|
||||
async with asyncio.timeout(self._audio_chunk_timeout):
|
||||
chunk = await self._audio_queue.get()
|
||||
if not chunk:
|
||||
break
|
||||
try:
|
||||
async with asyncio.timeout(self._audio_chunk_timeout):
|
||||
chunk = await self._audio_queue.get()
|
||||
if not chunk:
|
||||
_LOGGER.debug("STT stream got None")
|
||||
break
|
||||
|
||||
yield chunk
|
||||
except TimeoutError:
|
||||
_LOGGER.debug("STT Stream timed out")
|
||||
if not retry:
|
||||
_LOGGER.debug("No more retries, ending STT stream")
|
||||
break
|
||||
retry = False
|
||||
|
||||
# Play listening tone at the start of each cycle
|
||||
await self._play_tone(Tones.LISTENING, silence_before=0.2)
|
||||
@@ -385,6 +437,7 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol
|
||||
)
|
||||
|
||||
if self._pipeline_had_error:
|
||||
_LOGGER.debug("Pipeline error")
|
||||
self._pipeline_had_error = False
|
||||
await self._play_tone(Tones.ERROR)
|
||||
else:
|
||||
@@ -394,7 +447,14 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol
|
||||
# length of the TTS audio.
|
||||
await self._tts_done.wait()
|
||||
except TimeoutError:
|
||||
# This shouldn't happen anymore, we are detecting hang ups with a separate task
|
||||
_LOGGER.exception("Timeout error")
|
||||
self.disconnect() # caller hung up
|
||||
except asyncio.CancelledError:
|
||||
_LOGGER.debug("Pipeline cancelled")
|
||||
# Don't swallow cancellation
|
||||
if (current_task := asyncio.current_task()) and current_task.cancelling():
|
||||
raise
|
||||
finally:
|
||||
# Stop audio stream
|
||||
await self._audio_queue.put(None)
|
||||
@@ -433,8 +493,8 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol
|
||||
|
||||
if self._run_pipeline_after_announce:
|
||||
# Clear announcement to allow pipeline to run
|
||||
_LOGGER.debug("Clearing announcement")
|
||||
self._announcement = None
|
||||
self._announcement_future.set_result(None)
|
||||
|
||||
def _clear_audio_queue(self) -> None:
|
||||
"""Ensure audio queue is empty."""
|
||||
@@ -463,6 +523,7 @@ class VoipAssistSatellite(VoIPEntity, AssistSatelliteEntity, RtpDatagramProtocol
|
||||
)
|
||||
else:
|
||||
# Empty TTS response
|
||||
_LOGGER.debug("Empty TTS response")
|
||||
self._tts_done.set()
|
||||
elif event.type == PipelineEventType.ERROR:
|
||||
# Play error tone instead of wait for TTS when pipeline is finished.
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"domain": "voip",
|
||||
"name": "Voice over IP",
|
||||
"codeowners": ["@balloob", "@synesthesiam"],
|
||||
"codeowners": ["@balloob", "@synesthesiam", "@jaminh"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["assist_pipeline", "assist_satellite", "intent", "network"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/voip",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["voip_utils"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["voip-utils==0.3.1"]
|
||||
"requirements": ["voip-utils==0.3.2"]
|
||||
}
|
||||
|
||||
@@ -24,5 +24,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/xiaomi_ble",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["xiaomi-ble==0.37.0"]
|
||||
"requirements": ["xiaomi-ble==0.38.0"]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["zeroconf"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["zeroconf==0.146.5"]
|
||||
"requirements": ["zeroconf==0.147.0"]
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from contextlib import suppress
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from pathlib import Path
|
||||
@@ -77,6 +78,7 @@ ADDON_SETUP_TIMEOUT = 5
|
||||
ADDON_SETUP_TIMEOUT_ROUNDS = 40
|
||||
CONF_EMULATE_HARDWARE = "emulate_hardware"
|
||||
CONF_LOG_LEVEL = "log_level"
|
||||
RESTORE_NVM_DRIVER_READY_TIMEOUT = 60
|
||||
SERVER_VERSION_TIMEOUT = 10
|
||||
|
||||
ADDON_LOG_LEVELS = {
|
||||
@@ -461,10 +463,18 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
if vid == "10C4" and pid == "EA60" and description and "2652" in description:
|
||||
return self.async_abort(reason="not_zwave_device")
|
||||
|
||||
discovery_info.device = await self.hass.async_add_executor_job(
|
||||
usb.get_serial_by_id, discovery_info.device
|
||||
)
|
||||
|
||||
addon_info = await self._async_get_addon_info()
|
||||
if (
|
||||
addon_info.state not in (AddonState.NOT_INSTALLED, AddonState.INSTALLING)
|
||||
and addon_info.options.get(CONF_ADDON_DEVICE) == discovery_info.device
|
||||
and (addon_device := addon_info.options.get(CONF_ADDON_DEVICE)) is not None
|
||||
and await self.hass.async_add_executor_job(
|
||||
usb.get_serial_by_id, addon_device
|
||||
)
|
||||
== discovery_info.device
|
||||
):
|
||||
return self.async_abort(reason="already_configured")
|
||||
|
||||
@@ -717,7 +727,9 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
data_schema = vol.Schema(schema)
|
||||
|
||||
return self.async_show_form(step_id="configure_addon", data_schema=data_schema)
|
||||
return self.async_show_form(
|
||||
step_id="configure_addon_user", data_schema=data_schema
|
||||
)
|
||||
|
||||
async def async_step_finish_addon_setup_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -895,10 +907,6 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Reset the current controller, and instruct the user to unplug it."""
|
||||
|
||||
if user_input is not None:
|
||||
config_entry = self._reconfigure_config_entry
|
||||
assert config_entry is not None
|
||||
# Unload the config entry before stopping the add-on.
|
||||
await self.hass.config_entries.async_unload(config_entry.entry_id)
|
||||
if self.usb_path:
|
||||
# USB discovery was used, so the device is already known.
|
||||
await self._async_set_addon_config({CONF_ADDON_DEVICE: self.usb_path})
|
||||
@@ -913,6 +921,11 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
_LOGGER.error("Failed to reset controller: %s", err)
|
||||
return self.async_abort(reason="reset_failed")
|
||||
|
||||
config_entry = self._reconfigure_config_entry
|
||||
assert config_entry is not None
|
||||
# Unload the config entry before asking the user to unplug the controller.
|
||||
await self.hass.config_entries.async_unload(config_entry.entry_id)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="instruct_unplug",
|
||||
description_placeholders={
|
||||
@@ -1097,7 +1110,9 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
}
|
||||
)
|
||||
|
||||
return self.async_show_form(step_id="configure_addon", data_schema=data_schema)
|
||||
return self.async_show_form(
|
||||
step_id="configure_addon_reconfigure", data_schema=data_schema
|
||||
)
|
||||
|
||||
async def async_step_choose_serial_port(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -1305,15 +1320,28 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
event["bytesWritten"] / event["total"] * 0.5 + 0.5
|
||||
)
|
||||
|
||||
controller = self._get_driver().controller
|
||||
@callback
|
||||
def set_driver_ready(event: dict) -> None:
|
||||
"Set the driver ready event."
|
||||
wait_driver_ready.set()
|
||||
|
||||
driver = self._get_driver()
|
||||
controller = driver.controller
|
||||
wait_driver_ready = asyncio.Event()
|
||||
unsubs = [
|
||||
controller.on("nvm convert progress", forward_progress),
|
||||
controller.on("nvm restore progress", forward_progress),
|
||||
driver.once("driver ready", set_driver_ready),
|
||||
]
|
||||
try:
|
||||
await controller.async_restore_nvm(self.backup_data)
|
||||
except FailedCommand as err:
|
||||
raise AbortFlow(f"Failed to restore network: {err}") from err
|
||||
else:
|
||||
with suppress(TimeoutError):
|
||||
async with asyncio.timeout(RESTORE_NVM_DRIVER_READY_TIMEOUT):
|
||||
await wait_driver_ready.wait()
|
||||
await self.hass.config_entries.async_reload(config_entry.entry_id)
|
||||
finally:
|
||||
for unsub in unsubs:
|
||||
unsub()
|
||||
|
||||
@@ -37,8 +37,10 @@
|
||||
"restore_nvm": "Please wait while the network restore completes."
|
||||
},
|
||||
"step": {
|
||||
"configure_addon": {
|
||||
"configure_addon_user": {
|
||||
"data": {
|
||||
"lr_s2_access_control_key": "Long Range S2 Access Control Key",
|
||||
"lr_s2_authenticated_key": "Long Range S2 Authenticated Key",
|
||||
"s0_legacy_key": "S0 Key (Legacy)",
|
||||
"s2_access_control_key": "S2 Access Control Key",
|
||||
"s2_authenticated_key": "S2 Authenticated Key",
|
||||
@@ -52,14 +54,16 @@
|
||||
"data": {
|
||||
"emulate_hardware": "Emulate Hardware",
|
||||
"log_level": "Log level",
|
||||
"s0_legacy_key": "[%key:component::zwave_js::config::step::configure_addon::data::s0_legacy_key%]",
|
||||
"s2_access_control_key": "[%key:component::zwave_js::config::step::configure_addon::data::s2_access_control_key%]",
|
||||
"s2_authenticated_key": "[%key:component::zwave_js::config::step::configure_addon::data::s2_authenticated_key%]",
|
||||
"s2_unauthenticated_key": "[%key:component::zwave_js::config::step::configure_addon::data::s2_unauthenticated_key%]",
|
||||
"lr_s2_access_control_key": "[%key:component::zwave_js::config::step::configure_addon_user::data::lr_s2_access_control_key%]",
|
||||
"lr_s2_authenticated_key": "[%key:component::zwave_js::config::step::configure_addon_user::data::lr_s2_authenticated_key%]",
|
||||
"s0_legacy_key": "[%key:component::zwave_js::config::step::configure_addon_user::data::s0_legacy_key%]",
|
||||
"s2_access_control_key": "[%key:component::zwave_js::config::step::configure_addon_user::data::s2_access_control_key%]",
|
||||
"s2_authenticated_key": "[%key:component::zwave_js::config::step::configure_addon_user::data::s2_authenticated_key%]",
|
||||
"s2_unauthenticated_key": "[%key:component::zwave_js::config::step::configure_addon_user::data::s2_unauthenticated_key%]",
|
||||
"usb_path": "[%key:common::config_flow::data::usb_path%]"
|
||||
},
|
||||
"description": "[%key:component::zwave_js::config::step::configure_addon::description%]",
|
||||
"title": "[%key:component::zwave_js::config::step::configure_addon::title%]"
|
||||
"description": "[%key:component::zwave_js::config::step::configure_addon_user::description%]",
|
||||
"title": "[%key:component::zwave_js::config::step::configure_addon_user::title%]"
|
||||
},
|
||||
"hassio_confirm": {
|
||||
"description": "Do you want to set up the Z-Wave integration with the Z-Wave add-on?"
|
||||
|
||||
@@ -25,7 +25,7 @@ if TYPE_CHECKING:
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2025
|
||||
MINOR_VERSION: Final = 5
|
||||
PATCH_VERSION: Final = "0b2"
|
||||
PATCH_VERSION: Final = "0b8"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2)
|
||||
|
||||
Generated
+1
-1
@@ -75,6 +75,7 @@ FLOWS = {
|
||||
"aussie_broadband",
|
||||
"autarco",
|
||||
"awair",
|
||||
"aws_s3",
|
||||
"axis",
|
||||
"azure_data_explorer",
|
||||
"azure_devops",
|
||||
@@ -541,7 +542,6 @@ FLOWS = {
|
||||
"ruuvi_gateway",
|
||||
"ruuvitag_ble",
|
||||
"rympro",
|
||||
"s3",
|
||||
"sabnzbd",
|
||||
"samsungtv",
|
||||
"sanix",
|
||||
|
||||
@@ -219,6 +219,12 @@
|
||||
"iot_class": "cloud_push",
|
||||
"name": "Amazon Web Services (AWS)"
|
||||
},
|
||||
"aws_s3": {
|
||||
"integration_type": "service",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_push",
|
||||
"name": "AWS S3"
|
||||
},
|
||||
"fire_tv": {
|
||||
"integration_type": "virtual",
|
||||
"config_flow": false,
|
||||
@@ -5622,12 +5628,6 @@
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
"s3": {
|
||||
"name": "S3",
|
||||
"integration_type": "service",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_push"
|
||||
},
|
||||
"sabnzbd": {
|
||||
"name": "SABnzbd",
|
||||
"integration_type": "hub",
|
||||
|
||||
@@ -34,11 +34,11 @@ dbus-fast==2.43.0
|
||||
fnv-hash-fast==1.5.0
|
||||
go2rtc-client==0.1.2
|
||||
ha-ffmpeg==3.2.2
|
||||
habluetooth==3.47.1
|
||||
habluetooth==3.48.2
|
||||
hass-nabucasa==0.96.0
|
||||
hassil==2.2.3
|
||||
home-assistant-bluetooth==1.13.1
|
||||
home-assistant-frontend==20250502.0
|
||||
home-assistant-frontend==20250506.0
|
||||
home-assistant-intents==2025.4.30
|
||||
httpx==0.28.1
|
||||
ifaddr==0.2.0
|
||||
@@ -75,7 +75,7 @@ voluptuous-serialize==2.6.0
|
||||
voluptuous==0.15.2
|
||||
webrtc-models==0.3.0
|
||||
yarl==1.20.0
|
||||
zeroconf==0.146.5
|
||||
zeroconf==0.147.0
|
||||
|
||||
# Constrain pycryptodome to avoid vulnerability
|
||||
# see https://github.com/home-assistant/core/pull/16238
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2025.5.0b2"
|
||||
version = "2025.5.0b8"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
@@ -123,7 +123,7 @@ dependencies = [
|
||||
"voluptuous-openapi==0.0.7",
|
||||
"yarl==1.20.0",
|
||||
"webrtc-models==0.3.0",
|
||||
"zeroconf==0.146.5",
|
||||
"zeroconf==0.147.0",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
|
||||
Generated
+1
-1
@@ -60,4 +60,4 @@ voluptuous-serialize==2.6.0
|
||||
voluptuous-openapi==0.0.7
|
||||
yarl==1.20.0
|
||||
webrtc-models==0.3.0
|
||||
zeroconf==0.146.5
|
||||
zeroconf==0.147.0
|
||||
|
||||
Generated
+16
-16
@@ -210,7 +210,7 @@ aioazuredevops==2.2.1
|
||||
aiobafi6==0.9.0
|
||||
|
||||
# homeassistant.components.aws
|
||||
# homeassistant.components.s3
|
||||
# homeassistant.components.aws_s3
|
||||
aiobotocore==2.21.1
|
||||
|
||||
# homeassistant.components.comelit
|
||||
@@ -286,7 +286,7 @@ aiokafka==0.10.0
|
||||
aiokef==0.2.16
|
||||
|
||||
# homeassistant.components.rehlko
|
||||
aiokem==0.5.9
|
||||
aiokem==0.5.10
|
||||
|
||||
# homeassistant.components.lifx
|
||||
aiolifx-effects==0.3.2
|
||||
@@ -628,7 +628,7 @@ blockchain==1.4.4
|
||||
bluecurrent-api==1.2.3
|
||||
|
||||
# homeassistant.components.bluemaestro
|
||||
bluemaestro-ble==0.4.0
|
||||
bluemaestro-ble==0.4.1
|
||||
|
||||
# homeassistant.components.decora
|
||||
# bluepy==1.3.0
|
||||
@@ -1118,7 +1118,7 @@ ha-silabs-firmware-client==0.2.0
|
||||
habiticalib==0.3.7
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
habluetooth==3.47.1
|
||||
habluetooth==3.48.2
|
||||
|
||||
# homeassistant.components.cloud
|
||||
hass-nabucasa==0.96.0
|
||||
@@ -1161,13 +1161,13 @@ hole==0.8.0
|
||||
holidays==0.70
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20250502.0
|
||||
home-assistant-frontend==20250506.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2025.4.30
|
||||
|
||||
# homeassistant.components.homematicip_cloud
|
||||
homematicip==2.0.1
|
||||
homematicip==2.0.1.1
|
||||
|
||||
# homeassistant.components.horizon
|
||||
horimote==0.4.1
|
||||
@@ -1200,7 +1200,7 @@ ibmiotf==0.3.4
|
||||
# homeassistant.components.local_calendar
|
||||
# homeassistant.components.local_todo
|
||||
# homeassistant.components.remote_calendar
|
||||
ical==9.1.0
|
||||
ical==9.2.0
|
||||
|
||||
# homeassistant.components.caldav
|
||||
icalendar==6.1.0
|
||||
@@ -2093,7 +2093,7 @@ pykwb==0.0.8
|
||||
pylacrosse==0.4
|
||||
|
||||
# homeassistant.components.lamarzocco
|
||||
pylamarzocco==2.0.0b7
|
||||
pylamarzocco==2.0.0
|
||||
|
||||
# homeassistant.components.lastfm
|
||||
pylast==5.1.0
|
||||
@@ -2135,7 +2135,7 @@ pymeteoclimatic==0.1.0
|
||||
pymicro-vad==1.0.1
|
||||
|
||||
# homeassistant.components.miele
|
||||
pymiele==0.4.1
|
||||
pymiele==0.4.3
|
||||
|
||||
# homeassistant.components.xiaomi_tv
|
||||
pymitv==1.4.3
|
||||
@@ -2480,7 +2480,7 @@ python-rabbitair==0.0.8
|
||||
python-ripple-api==0.0.3
|
||||
|
||||
# homeassistant.components.roborock
|
||||
python-roborock==2.16.1
|
||||
python-roborock==2.18.2
|
||||
|
||||
# homeassistant.components.smarttub
|
||||
python-smarttub==0.0.39
|
||||
@@ -2631,7 +2631,7 @@ refoss-ha==1.2.5
|
||||
regenmaschine==2024.03.0
|
||||
|
||||
# homeassistant.components.renault
|
||||
renault-api==0.3.0
|
||||
renault-api==0.3.1
|
||||
|
||||
# homeassistant.components.renson
|
||||
renson-endura-delta==1.7.2
|
||||
@@ -2975,7 +2975,7 @@ typedmonarchmoney==0.4.4
|
||||
uasiren==0.0.1
|
||||
|
||||
# homeassistant.components.unifiprotect
|
||||
uiprotect==7.5.5
|
||||
uiprotect==7.6.0
|
||||
|
||||
# homeassistant.components.landisgyr_heat_meter
|
||||
ultraheat-api==0.5.7
|
||||
@@ -3007,7 +3007,7 @@ url-normalize==2.2.1
|
||||
uvcclient==0.12.1
|
||||
|
||||
# homeassistant.components.roborock
|
||||
vacuum-map-parser-roborock==0.1.2
|
||||
vacuum-map-parser-roborock==0.1.4
|
||||
|
||||
# homeassistant.components.vallox
|
||||
vallox-websocket-api==5.3.0
|
||||
@@ -3025,7 +3025,7 @@ venstarcolortouch==0.19
|
||||
vilfo-api-client==0.5.0
|
||||
|
||||
# homeassistant.components.voip
|
||||
voip-utils==0.3.1
|
||||
voip-utils==0.3.2
|
||||
|
||||
# homeassistant.components.volkszaehler
|
||||
volkszaehler==0.4.0
|
||||
@@ -3101,7 +3101,7 @@ wyoming==1.5.4
|
||||
xbox-webapi==2.1.0
|
||||
|
||||
# homeassistant.components.xiaomi_ble
|
||||
xiaomi-ble==0.37.0
|
||||
xiaomi-ble==0.38.0
|
||||
|
||||
# homeassistant.components.knx
|
||||
xknx==3.6.0
|
||||
@@ -3156,7 +3156,7 @@ zabbix-utils==2.0.2
|
||||
zamg==0.3.6
|
||||
|
||||
# homeassistant.components.zeroconf
|
||||
zeroconf==0.146.5
|
||||
zeroconf==0.147.0
|
||||
|
||||
# homeassistant.components.zeversolar
|
||||
zeversolar==0.3.2
|
||||
|
||||
Generated
+16
-16
@@ -198,7 +198,7 @@ aioazuredevops==2.2.1
|
||||
aiobafi6==0.9.0
|
||||
|
||||
# homeassistant.components.aws
|
||||
# homeassistant.components.s3
|
||||
# homeassistant.components.aws_s3
|
||||
aiobotocore==2.21.1
|
||||
|
||||
# homeassistant.components.comelit
|
||||
@@ -268,7 +268,7 @@ aioimaplib==2.0.1
|
||||
aiokafka==0.10.0
|
||||
|
||||
# homeassistant.components.rehlko
|
||||
aiokem==0.5.9
|
||||
aiokem==0.5.10
|
||||
|
||||
# homeassistant.components.lifx
|
||||
aiolifx-effects==0.3.2
|
||||
@@ -556,7 +556,7 @@ blinkpy==0.23.0
|
||||
bluecurrent-api==1.2.3
|
||||
|
||||
# homeassistant.components.bluemaestro
|
||||
bluemaestro-ble==0.4.0
|
||||
bluemaestro-ble==0.4.1
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bluetooth-adapters==0.21.4
|
||||
@@ -960,7 +960,7 @@ ha-silabs-firmware-client==0.2.0
|
||||
habiticalib==0.3.7
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
habluetooth==3.47.1
|
||||
habluetooth==3.48.2
|
||||
|
||||
# homeassistant.components.cloud
|
||||
hass-nabucasa==0.96.0
|
||||
@@ -991,13 +991,13 @@ hole==0.8.0
|
||||
holidays==0.70
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20250502.0
|
||||
home-assistant-frontend==20250506.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2025.4.30
|
||||
|
||||
# homeassistant.components.homematicip_cloud
|
||||
homematicip==2.0.1
|
||||
homematicip==2.0.1.1
|
||||
|
||||
# homeassistant.components.remember_the_milk
|
||||
httplib2==0.20.4
|
||||
@@ -1021,7 +1021,7 @@ ibeacon-ble==1.2.0
|
||||
# homeassistant.components.local_calendar
|
||||
# homeassistant.components.local_todo
|
||||
# homeassistant.components.remote_calendar
|
||||
ical==9.1.0
|
||||
ical==9.2.0
|
||||
|
||||
# homeassistant.components.caldav
|
||||
icalendar==6.1.0
|
||||
@@ -1708,7 +1708,7 @@ pykrakenapi==0.1.8
|
||||
pykulersky==0.5.8
|
||||
|
||||
# homeassistant.components.lamarzocco
|
||||
pylamarzocco==2.0.0b7
|
||||
pylamarzocco==2.0.0
|
||||
|
||||
# homeassistant.components.lastfm
|
||||
pylast==5.1.0
|
||||
@@ -1747,7 +1747,7 @@ pymeteoclimatic==0.1.0
|
||||
pymicro-vad==1.0.1
|
||||
|
||||
# homeassistant.components.miele
|
||||
pymiele==0.4.1
|
||||
pymiele==0.4.3
|
||||
|
||||
# homeassistant.components.mochad
|
||||
pymochad==0.2.0
|
||||
@@ -2017,7 +2017,7 @@ python-picnic-api2==1.2.4
|
||||
python-rabbitair==0.0.8
|
||||
|
||||
# homeassistant.components.roborock
|
||||
python-roborock==2.16.1
|
||||
python-roborock==2.18.2
|
||||
|
||||
# homeassistant.components.smarttub
|
||||
python-smarttub==0.0.39
|
||||
@@ -2138,7 +2138,7 @@ refoss-ha==1.2.5
|
||||
regenmaschine==2024.03.0
|
||||
|
||||
# homeassistant.components.renault
|
||||
renault-api==0.3.0
|
||||
renault-api==0.3.1
|
||||
|
||||
# homeassistant.components.renson
|
||||
renson-endura-delta==1.7.2
|
||||
@@ -2404,7 +2404,7 @@ typedmonarchmoney==0.4.4
|
||||
uasiren==0.0.1
|
||||
|
||||
# homeassistant.components.unifiprotect
|
||||
uiprotect==7.5.5
|
||||
uiprotect==7.6.0
|
||||
|
||||
# homeassistant.components.landisgyr_heat_meter
|
||||
ultraheat-api==0.5.7
|
||||
@@ -2430,7 +2430,7 @@ url-normalize==2.2.1
|
||||
uvcclient==0.12.1
|
||||
|
||||
# homeassistant.components.roborock
|
||||
vacuum-map-parser-roborock==0.1.2
|
||||
vacuum-map-parser-roborock==0.1.4
|
||||
|
||||
# homeassistant.components.vallox
|
||||
vallox-websocket-api==5.3.0
|
||||
@@ -2448,7 +2448,7 @@ venstarcolortouch==0.19
|
||||
vilfo-api-client==0.5.0
|
||||
|
||||
# homeassistant.components.voip
|
||||
voip-utils==0.3.1
|
||||
voip-utils==0.3.2
|
||||
|
||||
# homeassistant.components.volvooncall
|
||||
volvooncall==0.10.3
|
||||
@@ -2509,7 +2509,7 @@ wyoming==1.5.4
|
||||
xbox-webapi==2.1.0
|
||||
|
||||
# homeassistant.components.xiaomi_ble
|
||||
xiaomi-ble==0.37.0
|
||||
xiaomi-ble==0.38.0
|
||||
|
||||
# homeassistant.components.knx
|
||||
xknx==3.6.0
|
||||
@@ -2555,7 +2555,7 @@ yt-dlp[default]==2025.03.31
|
||||
zamg==0.3.6
|
||||
|
||||
# homeassistant.components.zeroconf
|
||||
zeroconf==0.146.5
|
||||
zeroconf==0.147.0
|
||||
|
||||
# homeassistant.components.zeversolar
|
||||
zeversolar==0.3.2
|
||||
|
||||
@@ -208,7 +208,6 @@ EXCEPTIONS = {
|
||||
# https://github.com/jaraco/skeleton/pull/170
|
||||
# https://github.com/jaraco/skeleton/pull/171
|
||||
"jaraco.itertools", # MIT - https://github.com/jaraco/jaraco.itertools/issues/21
|
||||
"setuptools", # MIT
|
||||
}
|
||||
|
||||
TODO = {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Tests for the S3 integration."""
|
||||
"""Tests for the AWS S3 integration."""
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Common fixtures for the S3 tests."""
|
||||
"""Common fixtures for the AWS S3 tests."""
|
||||
|
||||
from collections.abc import AsyncIterator, Generator
|
||||
import json
|
||||
@@ -6,12 +6,12 @@ from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.backup import AgentBackup
|
||||
from homeassistant.components.s3.backup import (
|
||||
from homeassistant.components.aws_s3.backup import (
|
||||
MULTIPART_MIN_PART_SIZE_BYTES,
|
||||
suggested_filenames,
|
||||
)
|
||||
from homeassistant.components.s3.const import DOMAIN
|
||||
from homeassistant.components.aws_s3.const import DOMAIN
|
||||
from homeassistant.components.backup import AgentBackup
|
||||
|
||||
from .const import USER_INPUT
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Consts for S3 tests."""
|
||||
"""Consts for AWS S3 tests."""
|
||||
|
||||
from homeassistant.components.s3.const import (
|
||||
from homeassistant.components.aws_s3.const import (
|
||||
CONF_ACCESS_KEY_ID,
|
||||
CONF_BUCKET,
|
||||
CONF_ENDPOINT_URL,
|
||||
@@ -10,6 +10,6 @@ from homeassistant.components.s3.const import (
|
||||
USER_INPUT = {
|
||||
CONF_ACCESS_KEY_ID: "TestTestTestTestTest",
|
||||
CONF_SECRET_ACCESS_KEY: "TestTestTestTestTestTestTestTestTestTest",
|
||||
CONF_ENDPOINT_URL: "http://127.0.0.1:9000",
|
||||
CONF_ENDPOINT_URL: "https://s3.eu-south-1.amazonaws.com",
|
||||
CONF_BUCKET: "test",
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Test the S3 backup platform."""
|
||||
"""Test the AWS S3 backup platform."""
|
||||
|
||||
from collections.abc import AsyncGenerator
|
||||
from io import StringIO
|
||||
@@ -9,19 +9,19 @@ from unittest.mock import AsyncMock, Mock, patch
|
||||
from botocore.exceptions import ConnectTimeoutError
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.backup import DOMAIN as BACKUP_DOMAIN, AgentBackup
|
||||
from homeassistant.components.s3.backup import (
|
||||
from homeassistant.components.aws_s3.backup import (
|
||||
MULTIPART_MIN_PART_SIZE_BYTES,
|
||||
BotoCoreError,
|
||||
S3BackupAgent,
|
||||
async_register_backup_agents_listener,
|
||||
suggested_filenames,
|
||||
)
|
||||
from homeassistant.components.s3.const import (
|
||||
from homeassistant.components.aws_s3.const import (
|
||||
CONF_ENDPOINT_URL,
|
||||
DATA_BACKUP_AGENT_LISTENERS,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.components.backup import DOMAIN as BACKUP_DOMAIN, AgentBackup
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.backup import async_initialize_backup
|
||||
from homeassistant.setup import async_setup_component
|
||||
@@ -362,7 +362,7 @@ async def test_agents_upload_network_failure(
|
||||
)
|
||||
|
||||
assert resp.status == 201
|
||||
assert "Upload failed for s3" in caplog.text
|
||||
assert "Upload failed for aws_s3" in caplog.text
|
||||
|
||||
|
||||
async def test_agents_download(
|
||||
+28
-3
@@ -1,4 +1,4 @@
|
||||
"""Test the S3 config flow."""
|
||||
"""Test the AWS S3 config flow."""
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
@@ -10,7 +10,7 @@ from botocore.exceptions import (
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.s3.const import CONF_BUCKET, CONF_ENDPOINT_URL, DOMAIN
|
||||
from homeassistant.components.aws_s3.const import CONF_BUCKET, CONF_ENDPOINT_URL, DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
@@ -21,8 +21,12 @@ from tests.common import MockConfigEntry
|
||||
|
||||
async def _async_start_flow(
|
||||
hass: HomeAssistant,
|
||||
user_input: dict[str, str] | None = None,
|
||||
) -> FlowResultType:
|
||||
"""Initialize the config flow."""
|
||||
if user_input is None:
|
||||
user_input = USER_INPUT
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
@@ -30,7 +34,7 @@ async def _async_start_flow(
|
||||
|
||||
return await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
USER_INPUT,
|
||||
user_input,
|
||||
)
|
||||
|
||||
|
||||
@@ -116,3 +120,24 @@ async def test_abort_if_already_configured(
|
||||
result = await _async_start_flow(hass)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_flow_create_not_aws_endpoint(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test config flow with a not aws endpoint should raise an error."""
|
||||
result = await _async_start_flow(
|
||||
hass, USER_INPUT | {CONF_ENDPOINT_URL: "http://example.com"}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {CONF_ENDPOINT_URL: "invalid_endpoint_url"}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
USER_INPUT,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "test"
|
||||
assert result["data"] == USER_INPUT
|
||||
@@ -1,8 +1,7 @@
|
||||
"""Test the s3 storage integration."""
|
||||
"""Test the AWS S3 storage integration."""
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from botocore.config import Config
|
||||
from botocore.exceptions import (
|
||||
ClientError,
|
||||
EndpointConnectionError,
|
||||
@@ -74,19 +73,3 @@ async def test_setup_entry_head_bucket_error(
|
||||
)
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
|
||||
|
||||
|
||||
async def test_checksum_settings_present(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test that checksum validation is set to be compatible with third-party S3 providers."""
|
||||
# due to https://github.com/home-assistant/core/issues/143995
|
||||
with patch(
|
||||
"homeassistant.components.s3.AioSession.create_client"
|
||||
) as mock_create_client:
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
config_arg = mock_create_client.call_args[1]["config"]
|
||||
assert isinstance(config_arg, Config)
|
||||
assert config_arg.request_checksum_calculation == "when_required"
|
||||
assert config_arg.response_checksum_validation == "when_required"
|
||||
@@ -10,8 +10,11 @@ from aioesphomeapi import (
|
||||
BinarySensorState,
|
||||
SensorInfo,
|
||||
SensorState,
|
||||
build_unique_id,
|
||||
)
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.esphome import DOMAIN
|
||||
from homeassistant.const import (
|
||||
ATTR_FRIENDLY_NAME,
|
||||
ATTR_RESTORED,
|
||||
@@ -19,6 +22,7 @@ from homeassistant.const import (
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
STATE_UNAVAILABLE,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
@@ -513,3 +517,151 @@ async def test_entity_without_name_device_with_friendly_name(
|
||||
# Make sure we have set the name to `None` as otherwise
|
||||
# the friendly_name will be "The Best Mixer "
|
||||
assert state.attributes[ATTR_FRIENDLY_NAME] == "The Best Mixer"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("hass_storage")
|
||||
async def test_entity_id_preserved_on_upgrade(
|
||||
hass: HomeAssistant,
|
||||
mock_client: APIClient,
|
||||
mock_esphome_device: MockESPHomeDeviceType,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test entity_id is preserved on upgrade."""
|
||||
entity_info = [
|
||||
BinarySensorInfo(
|
||||
object_id="my",
|
||||
key=1,
|
||||
name="my",
|
||||
unique_id="binary_sensor_my",
|
||||
),
|
||||
]
|
||||
states = [
|
||||
BinarySensorState(key=1, state=True, missing_state=False),
|
||||
]
|
||||
user_service = []
|
||||
assert (
|
||||
build_unique_id("11:22:33:44:55:AA", entity_info[0])
|
||||
== "11:22:33:44:55:AA-binary_sensor-my"
|
||||
)
|
||||
|
||||
entry = entity_registry.async_get_or_create(
|
||||
Platform.BINARY_SENSOR,
|
||||
DOMAIN,
|
||||
"11:22:33:44:55:AA-binary_sensor-my",
|
||||
suggested_object_id="should_not_change",
|
||||
)
|
||||
assert entry.entity_id == "binary_sensor.should_not_change"
|
||||
await mock_esphome_device(
|
||||
mock_client=mock_client,
|
||||
entity_info=entity_info,
|
||||
user_service=user_service,
|
||||
states=states,
|
||||
device_info={"friendly_name": "The Best Mixer", "name": "mixer"},
|
||||
)
|
||||
state = hass.states.get("binary_sensor.should_not_change")
|
||||
assert state is not None
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("hass_storage")
|
||||
async def test_entity_id_preserved_on_upgrade_old_format_entity_id(
|
||||
hass: HomeAssistant,
|
||||
mock_client: APIClient,
|
||||
mock_esphome_device: MockESPHomeDeviceType,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test entity_id is preserved on upgrade from old format."""
|
||||
entity_info = [
|
||||
BinarySensorInfo(
|
||||
object_id="my",
|
||||
key=1,
|
||||
name="my",
|
||||
unique_id="binary_sensor_my",
|
||||
),
|
||||
]
|
||||
states = [
|
||||
BinarySensorState(key=1, state=True, missing_state=False),
|
||||
]
|
||||
user_service = []
|
||||
assert (
|
||||
build_unique_id("11:22:33:44:55:AA", entity_info[0])
|
||||
== "11:22:33:44:55:AA-binary_sensor-my"
|
||||
)
|
||||
|
||||
entry = entity_registry.async_get_or_create(
|
||||
Platform.BINARY_SENSOR,
|
||||
DOMAIN,
|
||||
"11:22:33:44:55:AA-binary_sensor-my",
|
||||
suggested_object_id="my",
|
||||
)
|
||||
assert entry.entity_id == "binary_sensor.my"
|
||||
await mock_esphome_device(
|
||||
mock_client=mock_client,
|
||||
entity_info=entity_info,
|
||||
user_service=user_service,
|
||||
states=states,
|
||||
device_info={"name": "mixer"},
|
||||
)
|
||||
state = hass.states.get("binary_sensor.my")
|
||||
assert state is not None
|
||||
|
||||
|
||||
async def test_entity_id_preserved_on_upgrade_when_in_storage(
|
||||
hass: HomeAssistant,
|
||||
mock_client: APIClient,
|
||||
hass_storage: dict[str, Any],
|
||||
mock_esphome_device: MockESPHomeDeviceType,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test entity_id is preserved on upgrade with user defined entity_id."""
|
||||
entity_info = [
|
||||
BinarySensorInfo(
|
||||
object_id="my",
|
||||
key=1,
|
||||
name="my",
|
||||
unique_id="binary_sensor_my",
|
||||
),
|
||||
]
|
||||
states = [
|
||||
BinarySensorState(key=1, state=True, missing_state=False),
|
||||
]
|
||||
user_service = []
|
||||
device = await mock_esphome_device(
|
||||
mock_client=mock_client,
|
||||
entity_info=entity_info,
|
||||
user_service=user_service,
|
||||
states=states,
|
||||
device_info={"friendly_name": "The Best Mixer", "name": "mixer"},
|
||||
)
|
||||
state = hass.states.get("binary_sensor.mixer_my")
|
||||
assert state is not None
|
||||
# now rename the entity
|
||||
ent_reg_entry = entity_registry.async_get_or_create(
|
||||
Platform.BINARY_SENSOR,
|
||||
DOMAIN,
|
||||
"11:22:33:44:55:AA-binary_sensor-my",
|
||||
)
|
||||
entity_registry.async_update_entity(
|
||||
ent_reg_entry.entity_id,
|
||||
new_entity_id="binary_sensor.user_named",
|
||||
)
|
||||
await hass.config_entries.async_unload(device.entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
entry = device.entry
|
||||
entry_id = entry.entry_id
|
||||
storage_key = f"esphome.{entry_id}"
|
||||
assert len(hass_storage[storage_key]["data"]["binary_sensor"]) == 1
|
||||
binary_sensor_data: dict[str, Any] = hass_storage[storage_key]["data"][
|
||||
"binary_sensor"
|
||||
][0]
|
||||
assert binary_sensor_data["name"] == "my"
|
||||
assert binary_sensor_data["object_id"] == "my"
|
||||
device = await mock_esphome_device(
|
||||
mock_client=mock_client,
|
||||
entity_info=entity_info,
|
||||
user_service=user_service,
|
||||
states=states,
|
||||
entry=entry,
|
||||
device_info={"friendly_name": "The Best Mixer", "name": "mixer"},
|
||||
)
|
||||
state = hass.states.get("binary_sensor.user_named")
|
||||
assert state is not None
|
||||
|
||||
@@ -211,6 +211,8 @@ async def test_set_temperature(
|
||||
) -> None:
|
||||
"""Test setting temperature."""
|
||||
device = FritzDeviceClimateMock()
|
||||
device.lock = False
|
||||
|
||||
await setup_config_entry(
|
||||
hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz
|
||||
)
|
||||
@@ -288,6 +290,8 @@ async def test_set_hvac_mode(
|
||||
) -> None:
|
||||
"""Test setting hvac mode."""
|
||||
device = FritzDeviceClimateMock()
|
||||
|
||||
device.lock = False
|
||||
device.target_temperature = target_temperature
|
||||
|
||||
if current_preset is PRESET_COMFORT:
|
||||
@@ -335,6 +339,8 @@ async def test_set_preset_mode_comfort(
|
||||
) -> None:
|
||||
"""Test setting preset mode."""
|
||||
device = FritzDeviceClimateMock()
|
||||
|
||||
device.lock = False
|
||||
device.comfort_temperature = comfort_temperature
|
||||
await setup_config_entry(
|
||||
hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz
|
||||
@@ -366,6 +372,8 @@ async def test_set_preset_mode_eco(
|
||||
) -> None:
|
||||
"""Test setting preset mode."""
|
||||
device = FritzDeviceClimateMock()
|
||||
|
||||
device.lock = False
|
||||
device.eco_temperature = eco_temperature
|
||||
await setup_config_entry(
|
||||
hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz
|
||||
@@ -387,6 +395,8 @@ async def test_set_preset_mode_boost(
|
||||
) -> None:
|
||||
"""Test setting preset mode."""
|
||||
device = FritzDeviceClimateMock()
|
||||
device.lock = False
|
||||
|
||||
await setup_config_entry(
|
||||
hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz
|
||||
)
|
||||
@@ -471,11 +481,106 @@ async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
assert state
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"service_data",
|
||||
[
|
||||
{ATTR_TEMPERATURE: 23},
|
||||
{
|
||||
ATTR_HVAC_MODE: HVACMode.HEAT,
|
||||
ATTR_TEMPERATURE: 25,
|
||||
},
|
||||
],
|
||||
)
|
||||
async def test_set_temperature_lock(
|
||||
hass: HomeAssistant,
|
||||
fritz: Mock,
|
||||
service_data: dict,
|
||||
) -> None:
|
||||
"""Test setting temperature while device is locked."""
|
||||
device = FritzDeviceClimateMock()
|
||||
|
||||
device.lock = True
|
||||
assert await setup_config_entry(
|
||||
hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
HomeAssistantError,
|
||||
match="Can't change settings while manual access for telephone, app, or user interface is disabled on the device",
|
||||
):
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, **service_data},
|
||||
True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("service_data", "target_temperature", "current_preset", "expected_call_args"),
|
||||
[
|
||||
# mode off always sets target temperature to 0
|
||||
({ATTR_HVAC_MODE: HVACMode.OFF}, 22, PRESET_COMFORT, [call(0, True)]),
|
||||
({ATTR_HVAC_MODE: HVACMode.OFF}, 16, PRESET_ECO, [call(0, True)]),
|
||||
({ATTR_HVAC_MODE: HVACMode.OFF}, 16, None, [call(0, True)]),
|
||||
# mode heat sets target temperature based on current scheduled preset,
|
||||
# when not already in mode heat
|
||||
({ATTR_HVAC_MODE: HVACMode.HEAT}, 0.0, PRESET_COMFORT, [call(22, True)]),
|
||||
({ATTR_HVAC_MODE: HVACMode.HEAT}, 0.0, PRESET_ECO, [call(16, True)]),
|
||||
({ATTR_HVAC_MODE: HVACMode.HEAT}, 0.0, None, [call(22, True)]),
|
||||
# mode heat does not set target temperature, when already in mode heat
|
||||
({ATTR_HVAC_MODE: HVACMode.HEAT}, 16, PRESET_COMFORT, []),
|
||||
({ATTR_HVAC_MODE: HVACMode.HEAT}, 16, PRESET_ECO, []),
|
||||
({ATTR_HVAC_MODE: HVACMode.HEAT}, 16, None, []),
|
||||
({ATTR_HVAC_MODE: HVACMode.HEAT}, 22, PRESET_COMFORT, []),
|
||||
({ATTR_HVAC_MODE: HVACMode.HEAT}, 22, PRESET_ECO, []),
|
||||
({ATTR_HVAC_MODE: HVACMode.HEAT}, 22, None, []),
|
||||
],
|
||||
)
|
||||
async def test_set_hvac_mode_lock(
|
||||
hass: HomeAssistant,
|
||||
fritz: Mock,
|
||||
service_data: dict,
|
||||
target_temperature: float,
|
||||
current_preset: str,
|
||||
expected_call_args: list[_Call],
|
||||
) -> None:
|
||||
"""Test setting hvac mode while device is locked."""
|
||||
device = FritzDeviceClimateMock()
|
||||
|
||||
device.lock = True
|
||||
device.target_temperature = target_temperature
|
||||
|
||||
if current_preset is PRESET_COMFORT:
|
||||
device.nextchange_temperature = device.eco_temperature
|
||||
elif current_preset is PRESET_ECO:
|
||||
device.nextchange_temperature = device.comfort_temperature
|
||||
else:
|
||||
device.nextchange_endperiod = 0
|
||||
|
||||
assert await setup_config_entry(
|
||||
hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
HomeAssistantError,
|
||||
match="Can't change settings while manual access for telephone, app, or user interface is disabled on the device",
|
||||
):
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, **service_data},
|
||||
True,
|
||||
)
|
||||
|
||||
|
||||
async def test_holidy_summer_mode(
|
||||
hass: HomeAssistant, freezer: FrozenDateTimeFactory, fritz: Mock
|
||||
) -> None:
|
||||
"""Test holiday and summer mode."""
|
||||
device = FritzDeviceClimateMock()
|
||||
device.lock = False
|
||||
|
||||
await setup_config_entry(
|
||||
hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz
|
||||
)
|
||||
@@ -510,7 +615,7 @@ async def test_holidy_summer_mode(
|
||||
|
||||
with pytest.raises(
|
||||
HomeAssistantError,
|
||||
match="Can't change HVAC mode while holiday or summer mode is active on the device",
|
||||
match="Can't change settings while holiday or summer mode is active on the device",
|
||||
):
|
||||
await hass.services.async_call(
|
||||
"climate",
|
||||
@@ -520,7 +625,7 @@ async def test_holidy_summer_mode(
|
||||
)
|
||||
with pytest.raises(
|
||||
HomeAssistantError,
|
||||
match="Can't change preset while holiday or summer mode is active on the device",
|
||||
match="Can't change settings while holiday or summer mode is active on the device",
|
||||
):
|
||||
await hass.services.async_call(
|
||||
"climate",
|
||||
@@ -546,7 +651,7 @@ async def test_holidy_summer_mode(
|
||||
|
||||
with pytest.raises(
|
||||
HomeAssistantError,
|
||||
match="Can't change HVAC mode while holiday or summer mode is active on the device",
|
||||
match="Can't change settings while holiday or summer mode is active on the device",
|
||||
):
|
||||
await hass.services.async_call(
|
||||
"climate",
|
||||
@@ -556,7 +661,7 @@ async def test_holidy_summer_mode(
|
||||
)
|
||||
with pytest.raises(
|
||||
HomeAssistantError,
|
||||
match="Can't change preset while holiday or summer mode is active on the device",
|
||||
match="Can't change settings while holiday or summer mode is active on the device",
|
||||
):
|
||||
await hass.services.async_call(
|
||||
"climate",
|
||||
|
||||
@@ -1,4 +1,21 @@
|
||||
# serializer version: 1
|
||||
# name: test_generate_content_file_processing_succeeds
|
||||
list([
|
||||
tuple(
|
||||
'',
|
||||
tuple(
|
||||
),
|
||||
dict({
|
||||
'contents': list([
|
||||
'Describe this image from my doorbell camera',
|
||||
File(name='doorbell_snapshot.jpg', display_name=None, mime_type=None, size_bytes=None, create_time=None, expiration_time=None, update_time=None, sha256_hash=None, uri=None, download_uri=None, state=<FileState.ACTIVE: 'ACTIVE'>, source=None, video_metadata=None, error=None),
|
||||
File(name='context.txt', display_name=None, mime_type=None, size_bytes=None, create_time=None, expiration_time=None, update_time=None, sha256_hash=None, uri=None, download_uri=None, state=<FileState.PROCESSING: 'PROCESSING'>, source=None, video_metadata=None, error=None),
|
||||
]),
|
||||
'model': 'models/gemini-2.0-flash',
|
||||
}),
|
||||
),
|
||||
])
|
||||
# ---
|
||||
# name: test_generate_content_service_with_image
|
||||
list([
|
||||
tuple(
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from unittest.mock import AsyncMock, Mock, mock_open, patch
|
||||
|
||||
from google.genai.types import File, FileState
|
||||
import pytest
|
||||
from requests.exceptions import Timeout
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
@@ -91,6 +92,117 @@ async def test_generate_content_service_with_image(
|
||||
assert [tuple(mock_call) for mock_call in mock_generate.mock_calls] == snapshot
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_init_component")
|
||||
async def test_generate_content_file_processing_succeeds(
|
||||
hass: HomeAssistant, snapshot: SnapshotAssertion
|
||||
) -> None:
|
||||
"""Test generate content service."""
|
||||
stubbed_generated_content = (
|
||||
"A mail carrier is at your front door delivering a package"
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"google.genai.models.AsyncModels.generate_content",
|
||||
return_value=Mock(
|
||||
text=stubbed_generated_content,
|
||||
prompt_feedback=None,
|
||||
candidates=[Mock()],
|
||||
),
|
||||
) as mock_generate,
|
||||
patch("pathlib.Path.exists", return_value=True),
|
||||
patch.object(hass.config, "is_allowed_path", return_value=True),
|
||||
patch("builtins.open", mock_open(read_data="this is an image")),
|
||||
patch("mimetypes.guess_type", return_value=["image/jpeg"]),
|
||||
patch(
|
||||
"google.genai.files.Files.upload",
|
||||
side_effect=[
|
||||
File(name="doorbell_snapshot.jpg", state=FileState.ACTIVE),
|
||||
File(name="context.txt", state=FileState.PROCESSING),
|
||||
],
|
||||
),
|
||||
patch(
|
||||
"google.genai.files.AsyncFiles.get",
|
||||
side_effect=[
|
||||
File(name="context.txt", state=FileState.PROCESSING),
|
||||
File(name="context.txt", state=FileState.ACTIVE),
|
||||
],
|
||||
),
|
||||
):
|
||||
response = await hass.services.async_call(
|
||||
"google_generative_ai_conversation",
|
||||
"generate_content",
|
||||
{
|
||||
"prompt": "Describe this image from my doorbell camera",
|
||||
"filenames": ["doorbell_snapshot.jpg", "context.txt", "context.txt"],
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
|
||||
assert response == {
|
||||
"text": stubbed_generated_content,
|
||||
}
|
||||
assert [tuple(mock_call) for mock_call in mock_generate.mock_calls] == snapshot
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_init_component")
|
||||
async def test_generate_content_file_processing_fails(
|
||||
hass: HomeAssistant, snapshot: SnapshotAssertion
|
||||
) -> None:
|
||||
"""Test generate content service."""
|
||||
stubbed_generated_content = (
|
||||
"A mail carrier is at your front door delivering a package"
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"google.genai.models.AsyncModels.generate_content",
|
||||
return_value=Mock(
|
||||
text=stubbed_generated_content,
|
||||
prompt_feedback=None,
|
||||
candidates=[Mock()],
|
||||
),
|
||||
),
|
||||
patch("pathlib.Path.exists", return_value=True),
|
||||
patch.object(hass.config, "is_allowed_path", return_value=True),
|
||||
patch("builtins.open", mock_open(read_data="this is an image")),
|
||||
patch("mimetypes.guess_type", return_value=["image/jpeg"]),
|
||||
patch(
|
||||
"google.genai.files.Files.upload",
|
||||
side_effect=[
|
||||
File(name="doorbell_snapshot.jpg", state=FileState.ACTIVE),
|
||||
File(name="context.txt", state=FileState.PROCESSING),
|
||||
],
|
||||
),
|
||||
patch(
|
||||
"google.genai.files.AsyncFiles.get",
|
||||
side_effect=[
|
||||
File(name="context.txt", state=FileState.PROCESSING),
|
||||
File(
|
||||
name="context.txt",
|
||||
state=FileState.FAILED,
|
||||
error={"message": "File processing failed"},
|
||||
),
|
||||
],
|
||||
),
|
||||
pytest.raises(
|
||||
HomeAssistantError,
|
||||
match="File `context.txt` processing failed, reason: File processing failed",
|
||||
),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
"google_generative_ai_conversation",
|
||||
"generate_content",
|
||||
{
|
||||
"prompt": "Describe this image from my doorbell camera",
|
||||
"filenames": ["doorbell_snapshot.jpg", "context.txt", "context.txt"],
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_init_component")
|
||||
async def test_generate_content_service_error(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -269,6 +269,49 @@ async def test_ingress_request_options(
|
||||
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_PROTO]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"build_type",
|
||||
[
|
||||
("a3_vl", "test/beer/ping?index=1"),
|
||||
("core", "index.html"),
|
||||
("local", "panel/config"),
|
||||
("jk_921", "editor.php?idx=3&ping=5"),
|
||||
("fsadjf10312", ""),
|
||||
],
|
||||
)
|
||||
async def test_ingress_request_head(
|
||||
hassio_noauth_client, build_type, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test no auth needed for ."""
|
||||
aioclient_mock.head(
|
||||
f"http://127.0.0.1/ingress/{build_type[0]}/{build_type[1]}",
|
||||
text="test",
|
||||
)
|
||||
|
||||
resp = await hassio_noauth_client.head(
|
||||
f"/api/hassio_ingress/{build_type[0]}/{build_type[1]}",
|
||||
headers={"X-Test-Header": "beer"},
|
||||
)
|
||||
|
||||
# Check we got right response
|
||||
assert resp.status == HTTPStatus.OK
|
||||
body = await resp.text()
|
||||
assert body == "" # head does not return a body
|
||||
|
||||
# Check we forwarded command
|
||||
assert len(aioclient_mock.mock_calls) == 1
|
||||
assert X_AUTH_TOKEN not in aioclient_mock.mock_calls[-1][3]
|
||||
assert aioclient_mock.mock_calls[-1][3]["X-Hass-Source"] == "core.ingress"
|
||||
assert (
|
||||
aioclient_mock.mock_calls[-1][3]["X-Ingress-Path"]
|
||||
== f"/api/hassio_ingress/{build_type[0]}"
|
||||
)
|
||||
assert aioclient_mock.mock_calls[-1][3]["X-Test-Header"] == "beer"
|
||||
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_FOR]
|
||||
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_HOST]
|
||||
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_PROTO]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"build_type",
|
||||
[
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from homematicip.auth import Auth
|
||||
from homematicip.base.base_connection import HmipConnectionError
|
||||
from homematicip.connection.connection_context import ConnectionContext
|
||||
from homematicip.exceptions.connection_exceptions import HmipConnectionError
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
from homematicip.base.base_connection import HmipConnectionError
|
||||
from homematicip.connection.connection_context import ConnectionContext
|
||||
from homematicip.exceptions.connection_exceptions import HmipConnectionError
|
||||
|
||||
from homeassistant.components.homematicip_cloud.const import (
|
||||
CONF_ACCESSPOINT,
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.cover import SERVICE_OPEN_COVER
|
||||
from homeassistant.components.lock import SERVICE_LOCK
|
||||
from homeassistant.components.button import SERVICE_PRESS
|
||||
from homeassistant.components.cover import SERVICE_CLOSE_COVER, SERVICE_OPEN_COVER
|
||||
from homeassistant.components.lock import SERVICE_LOCK, SERVICE_UNLOCK
|
||||
from homeassistant.components.valve import SERVICE_CLOSE_VALVE, SERVICE_OPEN_VALVE
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_FRIENDLY_NAME,
|
||||
@@ -121,41 +123,130 @@ async def test_turn_on_intent(hass: HomeAssistant) -> None:
|
||||
assert call.data == {"entity_id": ["light.test_light"]}
|
||||
|
||||
|
||||
async def test_translated_turn_on_intent(
|
||||
@pytest.mark.parametrize("domain", ["button", "input_button"])
|
||||
async def test_turn_on_intent_button(
|
||||
hass: HomeAssistant, entity_registry: er.EntityRegistry, domain
|
||||
) -> None:
|
||||
"""Test HassTurnOn intent on button domains."""
|
||||
assert await async_setup_component(hass, "intent", {})
|
||||
|
||||
button = entity_registry.async_get_or_create(domain, "test", "button_uid")
|
||||
|
||||
hass.states.async_set(button.entity_id, "unknown")
|
||||
button_service_calls = async_mock_service(hass, domain, SERVICE_PRESS)
|
||||
|
||||
with pytest.raises(intent.IntentHandleError):
|
||||
await intent.async_handle(
|
||||
hass, "test", "HassTurnOff", {"name": {"value": button.entity_id}}
|
||||
)
|
||||
|
||||
await intent.async_handle(
|
||||
hass, "test", "HassTurnOn", {"name": {"value": button.entity_id}}
|
||||
)
|
||||
|
||||
assert len(button_service_calls) == 1
|
||||
call = button_service_calls[0]
|
||||
assert call.domain == domain
|
||||
assert call.service == SERVICE_PRESS
|
||||
assert call.data == {"entity_id": button.entity_id}
|
||||
|
||||
|
||||
async def test_turn_on_off_intent_valve(
|
||||
hass: HomeAssistant, entity_registry: er.EntityRegistry
|
||||
) -> None:
|
||||
"""Test HassTurnOn intent on domains which don't have the intent."""
|
||||
result = await async_setup_component(hass, "homeassistant", {})
|
||||
result = await async_setup_component(hass, "intent", {})
|
||||
await hass.async_block_till_done()
|
||||
assert result
|
||||
"""Test HassTurnOn/Off intent on valve domains."""
|
||||
assert await async_setup_component(hass, "intent", {})
|
||||
|
||||
valve = entity_registry.async_get_or_create("valve", "test", "valve_uid")
|
||||
|
||||
hass.states.async_set(valve.entity_id, "closed")
|
||||
open_calls = async_mock_service(hass, "valve", SERVICE_OPEN_VALVE)
|
||||
close_calls = async_mock_service(hass, "valve", SERVICE_CLOSE_VALVE)
|
||||
|
||||
await intent.async_handle(
|
||||
hass, "test", "HassTurnOn", {"name": {"value": valve.entity_id}}
|
||||
)
|
||||
|
||||
assert len(open_calls) == 1
|
||||
call = open_calls[0]
|
||||
assert call.domain == "valve"
|
||||
assert call.service == SERVICE_OPEN_VALVE
|
||||
assert call.data == {"entity_id": valve.entity_id}
|
||||
|
||||
await intent.async_handle(
|
||||
hass, "test", "HassTurnOff", {"name": {"value": valve.entity_id}}
|
||||
)
|
||||
|
||||
assert len(close_calls) == 1
|
||||
call = close_calls[0]
|
||||
assert call.domain == "valve"
|
||||
assert call.service == SERVICE_CLOSE_VALVE
|
||||
assert call.data == {"entity_id": valve.entity_id}
|
||||
|
||||
|
||||
async def test_turn_on_off_intent_cover(
|
||||
hass: HomeAssistant, entity_registry: er.EntityRegistry
|
||||
) -> None:
|
||||
"""Test HassTurnOn/Off intent on cover domains."""
|
||||
assert await async_setup_component(hass, "intent", {})
|
||||
|
||||
cover = entity_registry.async_get_or_create("cover", "test", "cover_uid")
|
||||
lock = entity_registry.async_get_or_create("lock", "test", "lock_uid")
|
||||
|
||||
hass.states.async_set(cover.entity_id, "closed")
|
||||
hass.states.async_set(lock.entity_id, "unlocked")
|
||||
cover_service_calls = async_mock_service(hass, "cover", SERVICE_OPEN_COVER)
|
||||
lock_service_calls = async_mock_service(hass, "lock", SERVICE_LOCK)
|
||||
open_calls = async_mock_service(hass, "cover", SERVICE_OPEN_COVER)
|
||||
close_calls = async_mock_service(hass, "cover", SERVICE_CLOSE_COVER)
|
||||
|
||||
await intent.async_handle(
|
||||
hass, "test", "HassTurnOn", {"name": {"value": cover.entity_id}}
|
||||
)
|
||||
|
||||
assert len(open_calls) == 1
|
||||
call = open_calls[0]
|
||||
assert call.domain == "cover"
|
||||
assert call.service == SERVICE_OPEN_COVER
|
||||
assert call.data == {"entity_id": cover.entity_id}
|
||||
|
||||
await intent.async_handle(
|
||||
hass, "test", "HassTurnOff", {"name": {"value": cover.entity_id}}
|
||||
)
|
||||
|
||||
assert len(close_calls) == 1
|
||||
call = close_calls[0]
|
||||
assert call.domain == "cover"
|
||||
assert call.service == SERVICE_CLOSE_COVER
|
||||
assert call.data == {"entity_id": cover.entity_id}
|
||||
|
||||
|
||||
async def test_turn_on_off_intent_lock(
|
||||
hass: HomeAssistant, entity_registry: er.EntityRegistry
|
||||
) -> None:
|
||||
"""Test HassTurnOn/Off intent on lock domains."""
|
||||
assert await async_setup_component(hass, "intent", {})
|
||||
|
||||
lock = entity_registry.async_get_or_create("lock", "test", "lock_uid")
|
||||
|
||||
hass.states.async_set(lock.entity_id, "locked")
|
||||
unlock_calls = async_mock_service(hass, "lock", SERVICE_UNLOCK)
|
||||
lock_calls = async_mock_service(hass, "lock", SERVICE_LOCK)
|
||||
|
||||
await intent.async_handle(
|
||||
hass, "test", "HassTurnOn", {"name": {"value": lock.entity_id}}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(cover_service_calls) == 1
|
||||
call = cover_service_calls[0]
|
||||
assert call.domain == "cover"
|
||||
assert call.service == "open_cover"
|
||||
assert call.data == {"entity_id": cover.entity_id}
|
||||
|
||||
assert len(lock_service_calls) == 1
|
||||
call = lock_service_calls[0]
|
||||
assert len(lock_calls) == 1
|
||||
call = lock_calls[0]
|
||||
assert call.domain == "lock"
|
||||
assert call.service == "lock"
|
||||
assert call.service == SERVICE_LOCK
|
||||
assert call.data == {"entity_id": lock.entity_id}
|
||||
|
||||
await intent.async_handle(
|
||||
hass, "test", "HassTurnOff", {"name": {"value": lock.entity_id}}
|
||||
)
|
||||
|
||||
assert len(unlock_calls) == 1
|
||||
call = unlock_calls[0]
|
||||
assert call.domain == "lock"
|
||||
assert call.service == SERVICE_UNLOCK
|
||||
assert call.data == {"entity_id": lock.entity_id}
|
||||
|
||||
|
||||
|
||||
@@ -104,19 +104,6 @@ async def test_unpause_media_player_intent(hass: HomeAssistant) -> None:
|
||||
assert call.service == SERVICE_MEDIA_PLAY
|
||||
assert call.data == {"entity_id": entity_id}
|
||||
|
||||
# Test if not paused
|
||||
hass.states.async_set(
|
||||
entity_id,
|
||||
STATE_PLAYING,
|
||||
)
|
||||
|
||||
with pytest.raises(intent.MatchFailedError):
|
||||
response = await intent.async_handle(
|
||||
hass,
|
||||
"test",
|
||||
media_player_intent.INTENT_MEDIA_UNPAUSE,
|
||||
)
|
||||
|
||||
|
||||
async def test_next_media_player_intent(hass: HomeAssistant) -> None:
|
||||
"""Test HassMediaNext intent for media players."""
|
||||
@@ -245,17 +232,6 @@ async def test_volume_media_player_intent(hass: HomeAssistant) -> None:
|
||||
assert call.service == SERVICE_VOLUME_SET
|
||||
assert call.data == {"entity_id": entity_id, "volume_level": 0.5}
|
||||
|
||||
# Test if not playing
|
||||
hass.states.async_set(entity_id, STATE_IDLE, attributes=attributes)
|
||||
|
||||
with pytest.raises(intent.MatchFailedError):
|
||||
response = await intent.async_handle(
|
||||
hass,
|
||||
"test",
|
||||
media_player_intent.INTENT_SET_VOLUME,
|
||||
{"volume_level": {"value": 50}},
|
||||
)
|
||||
|
||||
# Test feature not supported
|
||||
hass.states.async_set(
|
||||
entity_id,
|
||||
|
||||
@@ -87,6 +87,7 @@ MOCK_SUBENTRY_NOTIFY_COMPONENT2 = {
|
||||
MOCK_SUBENTRY_NOTIFY_COMPONENT_NO_NAME = {
|
||||
"5269352dd9534c908d22812ea5d714cd": {
|
||||
"platform": "notify",
|
||||
"name": None,
|
||||
"command_topic": "test-topic",
|
||||
"command_template": "{{ value }}",
|
||||
"entity_picture": "https://example.com/5269352dd9534c908d22812ea5d714cd",
|
||||
@@ -152,6 +153,10 @@ MOCK_SUBENTRY_LIGHT_BASIC_KELVIN_COMPONENT = {
|
||||
"state_topic": "test-topic",
|
||||
"color_temp_kelvin": True,
|
||||
"state_value_template": "{{ value_json.value }}",
|
||||
"brightness_scale": 255,
|
||||
"max_kelvin": 6535,
|
||||
"min_kelvin": 2000,
|
||||
"white_scale": 255,
|
||||
"entity_picture": "https://example.com/8131babc5e8d4f44b82e0761d39091a2",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2817,14 +2817,22 @@ async def test_migrate_of_incompatible_config_entry(
|
||||
},
|
||||
{"state_topic": "invalid_subscribe_topic"},
|
||||
),
|
||||
(
|
||||
{
|
||||
"command_topic": "test-topic",
|
||||
"light_brightness_settings": {
|
||||
"brightness_command_topic": "test-topic#invalid"
|
||||
},
|
||||
},
|
||||
{"light_brightness_settings": "invalid_publish_topic"},
|
||||
),
|
||||
(
|
||||
{
|
||||
"command_topic": "test-topic",
|
||||
"advanced_settings": {"max_kelvin": 2000, "min_kelvin": 2000},
|
||||
},
|
||||
{
|
||||
"max_kelvin": "max_below_min_kelvin",
|
||||
"min_kelvin": "max_below_min_kelvin",
|
||||
"advanced_settings": "max_below_min_kelvin",
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# serializer version: 1
|
||||
# name: test_calendar_examples[office365_invalid_tzid]
|
||||
list([
|
||||
dict({
|
||||
'description': None,
|
||||
'end': dict({
|
||||
'dateTime': '2024-04-26T15:00:00-06:00',
|
||||
}),
|
||||
'location': '',
|
||||
'recurrence_id': None,
|
||||
'rrule': None,
|
||||
'start': dict({
|
||||
'dateTime': '2024-04-26T14:00:00-06:00',
|
||||
}),
|
||||
'summary': 'Uffe',
|
||||
'uid': '040000008200E00074C5B7101A82E00800000000687C546B5596DA01000000000000000010000000309AE93C8C3A94489F90ADBEA30C2F2B',
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
@@ -1,11 +1,13 @@
|
||||
"""Tests for calendar platform of Remote Calendar."""
|
||||
|
||||
from datetime import datetime
|
||||
import pathlib
|
||||
import textwrap
|
||||
|
||||
from httpx import Response
|
||||
import pytest
|
||||
import respx
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -21,6 +23,13 @@ from .conftest import (
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
# Test data files with known calendars from various sources. You can add a new file
|
||||
# in the testdata directory and add it will be parsed and tested.
|
||||
TESTDATA_FILES = sorted(
|
||||
pathlib.Path("tests/components/remote_calendar/testdata/").glob("*.ics")
|
||||
)
|
||||
TESTDATA_IDS = [f.stem for f in TESTDATA_FILES]
|
||||
|
||||
|
||||
@respx.mock
|
||||
async def test_empty_calendar(
|
||||
@@ -392,3 +401,24 @@ async def test_all_day_iter_order(
|
||||
|
||||
events = await get_events("2022-10-06T00:00:00Z", "2022-10-09T00:00:00Z")
|
||||
assert [event["summary"] for event in events] == event_order
|
||||
|
||||
|
||||
@respx.mock
|
||||
@pytest.mark.parametrize("ics_filename", TESTDATA_FILES, ids=TESTDATA_IDS)
|
||||
async def test_calendar_examples(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
get_events: GetEventsFn,
|
||||
ics_filename: pathlib.Path,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test parsing known calendars form test data files."""
|
||||
respx.get(CALENDER_URL).mock(
|
||||
return_value=Response(
|
||||
status_code=200,
|
||||
text=ics_filename.read_text(),
|
||||
)
|
||||
)
|
||||
await setup_integration(hass, config_entry)
|
||||
events = await get_events("1997-07-14T00:00:00", "2025-07-01T00:00:00")
|
||||
assert events == snapshot
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
BEGIN:VCALENDAR
|
||||
METHOD:PUBLISH
|
||||
PRODID:Microsoft Exchange Server 2010
|
||||
VERSION:2.0
|
||||
X-WR-CALNAME:Kalender
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:W. Europe Standard Time
|
||||
BEGIN:STANDARD
|
||||
DTSTART:16010101T030000
|
||||
TZOFFSETFROM:+0200
|
||||
TZOFFSETTO:+0100
|
||||
RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10
|
||||
END:STANDARD
|
||||
BEGIN:DAYLIGHT
|
||||
DTSTART:16010101T020000
|
||||
TZOFFSETFROM:+0100
|
||||
TZOFFSETTO:+0200
|
||||
RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3
|
||||
END:DAYLIGHT
|
||||
END:VTIMEZONE
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:UTC
|
||||
BEGIN:STANDARD
|
||||
DTSTART:16010101T000000
|
||||
TZOFFSETFROM:+0000
|
||||
TZOFFSETTO:+0000
|
||||
END:STANDARD
|
||||
BEGIN:DAYLIGHT
|
||||
DTSTART:16010101T000000
|
||||
TZOFFSETFROM:+0000
|
||||
TZOFFSETTO:+0000
|
||||
END:DAYLIGHT
|
||||
END:VTIMEZONE
|
||||
BEGIN:VEVENT
|
||||
UID:040000008200E00074C5B7101A82E00800000000687C546B5596DA01000000000000000
|
||||
010000000309AE93C8C3A94489F90ADBEA30C2F2B
|
||||
SUMMARY:Uffe
|
||||
DTSTART;TZID=Customized Time Zone:20240426T140000
|
||||
DTEND;TZID=Customized Time Zone:20240426T150000
|
||||
CLASS:PUBLIC
|
||||
PRIORITY:5
|
||||
DTSTAMP:20250417T155647Z
|
||||
TRANSP:OPAQUE
|
||||
STATUS:CONFIRMED
|
||||
SEQUENCE:0
|
||||
LOCATION:
|
||||
X-MICROSOFT-CDO-APPT-SEQUENCE:0
|
||||
X-MICROSOFT-CDO-BUSYSTATUS:BUSY
|
||||
X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY
|
||||
X-MICROSOFT-CDO-ALLDAYEVENT:FALSE
|
||||
X-MICROSOFT-CDO-IMPORTANCE:1
|
||||
X-MICROSOFT-CDO-INSTTYPE:0
|
||||
X-MICROSOFT-DONOTFORWARDMEETING:FALSE
|
||||
X-MICROSOFT-DISALLOW-COUNTER:FALSE
|
||||
X-MICROSOFT-REQUESTEDATTENDANCEMODE:DEFAULT
|
||||
X-MICROSOFT-ISRESPONSEREQUESTED:FALSE
|
||||
END:VEVENT
|
||||
END:VCALENDAR
|
||||
@@ -1005,102 +1005,6 @@
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_driver_door-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'binary_sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'binary_sensor.reg_twingo_iii_driver_door',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <BinarySensorDeviceClass.DOOR: 'door'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Driver door',
|
||||
'platform': 'renault',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'driver_door_status',
|
||||
'unique_id': 'vf1twingoiiivin_driver_door_status',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_driver_door-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'door',
|
||||
'friendly_name': 'REG-TWINGO-III Driver door',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.reg_twingo_iii_driver_door',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_hatch-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'binary_sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'binary_sensor.reg_twingo_iii_hatch',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <BinarySensorDeviceClass.DOOR: 'door'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Hatch',
|
||||
'platform': 'renault',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'hatch_status',
|
||||
'unique_id': 'vf1twingoiiivin_hatch_status',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_hatch-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'door',
|
||||
'friendly_name': 'REG-TWINGO-III Hatch',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.reg_twingo_iii_hatch',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_hvac-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
@@ -1148,102 +1052,6 @@
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_lock-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'binary_sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'binary_sensor.reg_twingo_iii_lock',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <BinarySensorDeviceClass.LOCK: 'lock'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Lock',
|
||||
'platform': 'renault',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'vf1twingoiiivin_lock_status',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_lock-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'lock',
|
||||
'friendly_name': 'REG-TWINGO-III Lock',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.reg_twingo_iii_lock',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_passenger_door-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'binary_sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'binary_sensor.reg_twingo_iii_passenger_door',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <BinarySensorDeviceClass.DOOR: 'door'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Passenger door',
|
||||
'platform': 'renault',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'passenger_door_status',
|
||||
'unique_id': 'vf1twingoiiivin_passenger_door_status',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_passenger_door-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'door',
|
||||
'friendly_name': 'REG-TWINGO-III Passenger door',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.reg_twingo_iii_passenger_door',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_plug-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
@@ -1292,102 +1100,6 @@
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_rear_left_door-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'binary_sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'binary_sensor.reg_twingo_iii_rear_left_door',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <BinarySensorDeviceClass.DOOR: 'door'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Rear left door',
|
||||
'platform': 'renault',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'rear_left_door_status',
|
||||
'unique_id': 'vf1twingoiiivin_rear_left_door_status',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_rear_left_door-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'door',
|
||||
'friendly_name': 'REG-TWINGO-III Rear left door',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.reg_twingo_iii_rear_left_door',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_rear_right_door-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'binary_sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'binary_sensor.reg_twingo_iii_rear_right_door',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <BinarySensorDeviceClass.DOOR: 'door'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Rear right door',
|
||||
'platform': 'renault',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'rear_right_door_status',
|
||||
'unique_id': 'vf1twingoiiivin_rear_right_door_status',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensors[twingo_3_electric][binary_sensor.reg_twingo_iii_rear_right_door-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'door',
|
||||
'friendly_name': 'REG-TWINGO-III Rear right door',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.reg_twingo_iii_rear_right_door',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensors[zoe_40][binary_sensor.reg_zoe_40_charging-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
@@ -1579,102 +1291,6 @@
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_driver_door-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'binary_sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'binary_sensor.reg_zoe_50_driver_door',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <BinarySensorDeviceClass.DOOR: 'door'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Driver door',
|
||||
'platform': 'renault',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'driver_door_status',
|
||||
'unique_id': 'vf1zoe50vin_driver_door_status',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_driver_door-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'door',
|
||||
'friendly_name': 'REG-ZOE-50 Driver door',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.reg_zoe_50_driver_door',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_hatch-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'binary_sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'binary_sensor.reg_zoe_50_hatch',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <BinarySensorDeviceClass.DOOR: 'door'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Hatch',
|
||||
'platform': 'renault',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'hatch_status',
|
||||
'unique_id': 'vf1zoe50vin_hatch_status',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_hatch-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'door',
|
||||
'friendly_name': 'REG-ZOE-50 Hatch',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.reg_zoe_50_hatch',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_hvac-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
@@ -1722,102 +1338,6 @@
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_lock-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'binary_sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'binary_sensor.reg_zoe_50_lock',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <BinarySensorDeviceClass.LOCK: 'lock'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Lock',
|
||||
'platform': 'renault',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'vf1zoe50vin_lock_status',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_lock-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'lock',
|
||||
'friendly_name': 'REG-ZOE-50 Lock',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.reg_zoe_50_lock',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_passenger_door-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'binary_sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'binary_sensor.reg_zoe_50_passenger_door',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <BinarySensorDeviceClass.DOOR: 'door'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Passenger door',
|
||||
'platform': 'renault',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'passenger_door_status',
|
||||
'unique_id': 'vf1zoe50vin_passenger_door_status',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_passenger_door-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'door',
|
||||
'friendly_name': 'REG-ZOE-50 Passenger door',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.reg_zoe_50_passenger_door',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_plug-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
@@ -1866,99 +1386,3 @@
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_rear_left_door-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'binary_sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'binary_sensor.reg_zoe_50_rear_left_door',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <BinarySensorDeviceClass.DOOR: 'door'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Rear left door',
|
||||
'platform': 'renault',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'rear_left_door_status',
|
||||
'unique_id': 'vf1zoe50vin_rear_left_door_status',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_rear_left_door-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'door',
|
||||
'friendly_name': 'REG-ZOE-50 Rear left door',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.reg_zoe_50_rear_left_door',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_rear_right_door-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'binary_sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'binary_sensor.reg_zoe_50_rear_right_door',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <BinarySensorDeviceClass.DOOR: 'door'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Rear right door',
|
||||
'platform': 'renault',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'rear_right_door_status',
|
||||
'unique_id': 'vf1zoe50vin_rear_right_door_status',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_sensors[zoe_50][binary_sensor.reg_zoe_50_rear_right_door-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'door',
|
||||
'friendly_name': 'REG-ZOE-50 Rear right door',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.reg_zoe_50_rear_right_door',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
|
||||
@@ -3211,100 +3211,6 @@
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[twingo_3_electric][sensor.reg_twingo_iii_remote_engine_start-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.reg_twingo_iii_remote_engine_start',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Remote engine start',
|
||||
'platform': 'renault',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'res_state',
|
||||
'unique_id': 'vf1twingoiiivin_res_state',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[twingo_3_electric][sensor.reg_twingo_iii_remote_engine_start-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'REG-TWINGO-III Remote engine start',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.reg_twingo_iii_remote_engine_start',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[twingo_3_electric][sensor.reg_twingo_iii_remote_engine_start_code-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.reg_twingo_iii_remote_engine_start_code',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Remote engine start code',
|
||||
'platform': 'renault',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'res_state_code',
|
||||
'unique_id': 'vf1twingoiiivin_res_state_code',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[twingo_3_electric][sensor.reg_twingo_iii_remote_engine_start_code-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'REG-TWINGO-III Remote engine start code',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.reg_twingo_iii_remote_engine_start_code',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[zoe_40][sensor.reg_zoe_40_battery-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
@@ -4737,97 +4643,3 @@
|
||||
'state': 'unplugged',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[zoe_50][sensor.reg_zoe_50_remote_engine_start-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.reg_zoe_50_remote_engine_start',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Remote engine start',
|
||||
'platform': 'renault',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'res_state',
|
||||
'unique_id': 'vf1zoe50vin_res_state',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[zoe_50][sensor.reg_zoe_50_remote_engine_start-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'REG-ZOE-50 Remote engine start',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.reg_zoe_50_remote_engine_start',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'Stopped, ready for RES',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[zoe_50][sensor.reg_zoe_50_remote_engine_start_code-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.reg_zoe_50_remote_engine_start_code',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Remote engine start code',
|
||||
'platform': 'renault',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'res_state_code',
|
||||
'unique_id': 'vf1zoe50vin_res_state_code',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[zoe_50][sensor.reg_zoe_50_remote_engine_start_code-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'REG-ZOE-50 Remote engine start code',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.reg_zoe_50_remote_engine_start_code',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '10',
|
||||
})
|
||||
# ---
|
||||
|
||||
@@ -197,7 +197,7 @@ async def test_sensor_throttling_after_init(
|
||||
@pytest.mark.parametrize(
|
||||
("vehicle_type", "vehicle_count", "scan_interval"),
|
||||
[
|
||||
("zoe_50", 1, 420), # 7 coordinators => 7 minutes interval
|
||||
("zoe_50", 1, 300), # 5 coordinators => 5 minutes interval
|
||||
("captur_fuel", 1, 240), # 4 coordinators => 4 minutes interval
|
||||
("multi", 2, 480), # 8 coordinators => 8 minutes interval
|
||||
],
|
||||
@@ -236,7 +236,7 @@ async def test_dynamic_scan_interval(
|
||||
@pytest.mark.parametrize(
|
||||
("vehicle_type", "vehicle_count", "scan_interval"),
|
||||
[
|
||||
("zoe_50", 1, 300), # (7-2) coordinators => 5 minutes interval
|
||||
("zoe_50", 1, 240), # (6-2) coordinators => 4 minutes interval
|
||||
("captur_fuel", 1, 180), # (4-1) coordinators => 3 minutes interval
|
||||
("multi", 2, 360), # (8-2) coordinators => 6 minutes interval
|
||||
],
|
||||
|
||||
@@ -78,6 +78,7 @@ async def test_browsing(hass: HomeAssistant, setup: str) -> None:
|
||||
assert item_child.children is None
|
||||
assert item_child.can_play is False
|
||||
assert item_child.can_expand is True
|
||||
assert item_child.thumbnail == "https://brands.home-assistant.io/_/test/logo.png"
|
||||
|
||||
item_child = await media_source.async_browse_media(
|
||||
hass, item.children[0].media_content_id + "?message=bla"
|
||||
|
||||
@@ -335,9 +335,8 @@ async def test_pipeline(
|
||||
patch.object(satellite, "tts_response_finished", tts_response_finished),
|
||||
):
|
||||
satellite._tones = Tones(0)
|
||||
satellite.transport = Mock()
|
||||
satellite.connection_made(Mock())
|
||||
|
||||
satellite.connection_made(satellite.transport)
|
||||
assert satellite.state == AssistSatelliteState.IDLE
|
||||
|
||||
# Ensure audio queue is cleared before pipeline starts
|
||||
@@ -473,7 +472,7 @@ async def test_tts_timeout(
|
||||
for tone in Tones:
|
||||
satellite._tone_bytes[tone] = tone_bytes
|
||||
|
||||
satellite.transport = Mock()
|
||||
satellite.connection_made(Mock())
|
||||
satellite.send_audio = Mock()
|
||||
|
||||
original_send_tts = satellite._send_tts
|
||||
@@ -511,6 +510,7 @@ async def test_tts_wrong_extension(
|
||||
assert await async_setup_component(hass, "voip", {})
|
||||
|
||||
satellite = async_get_satellite_entity(hass, voip.DOMAIN, voip_device.voip_id)
|
||||
satellite.addr = ("192.168.1.1", 12345)
|
||||
assert isinstance(satellite, VoipAssistSatellite)
|
||||
|
||||
done = asyncio.Event()
|
||||
@@ -559,8 +559,6 @@ async def test_tts_wrong_extension(
|
||||
"homeassistant.components.assist_satellite.entity.async_pipeline_from_audio_stream",
|
||||
new=async_pipeline_from_audio_stream,
|
||||
):
|
||||
satellite.transport = Mock()
|
||||
|
||||
original_send_tts = satellite._send_tts
|
||||
|
||||
async def send_tts(*args, **kwargs):
|
||||
@@ -572,6 +570,8 @@ async def test_tts_wrong_extension(
|
||||
|
||||
satellite._send_tts = AsyncMock(side_effect=send_tts) # type: ignore[method-assign]
|
||||
|
||||
satellite.connection_made(Mock())
|
||||
|
||||
# silence
|
||||
satellite.on_chunk(bytes(_ONE_SECOND))
|
||||
|
||||
@@ -579,10 +579,18 @@ async def test_tts_wrong_extension(
|
||||
satellite.on_chunk(bytes([255] * _ONE_SECOND * 2))
|
||||
|
||||
# silence (assumes relaxed VAD sensitivity)
|
||||
satellite.on_chunk(bytes(_ONE_SECOND * 4))
|
||||
satellite.on_chunk(bytes(_ONE_SECOND))
|
||||
await asyncio.sleep(0.2)
|
||||
satellite.on_chunk(bytes(_ONE_SECOND))
|
||||
await asyncio.sleep(0.2)
|
||||
satellite.on_chunk(bytes(_ONE_SECOND))
|
||||
await asyncio.sleep(0.2)
|
||||
satellite.on_chunk(bytes(_ONE_SECOND))
|
||||
await asyncio.sleep(0.2)
|
||||
satellite.on_chunk(bytes(_ONE_SECOND))
|
||||
|
||||
# Wait for mock pipeline to exhaust the audio stream
|
||||
async with asyncio.timeout(1):
|
||||
async with asyncio.timeout(3):
|
||||
await done.wait()
|
||||
|
||||
|
||||
@@ -595,6 +603,7 @@ async def test_tts_wrong_wav_format(
|
||||
assert await async_setup_component(hass, "voip", {})
|
||||
|
||||
satellite = async_get_satellite_entity(hass, voip.DOMAIN, voip_device.voip_id)
|
||||
satellite.addr = ("192.168.1.1", 12345)
|
||||
assert isinstance(satellite, VoipAssistSatellite)
|
||||
|
||||
done = asyncio.Event()
|
||||
@@ -643,8 +652,6 @@ async def test_tts_wrong_wav_format(
|
||||
"homeassistant.components.assist_satellite.entity.async_pipeline_from_audio_stream",
|
||||
new=async_pipeline_from_audio_stream,
|
||||
):
|
||||
satellite.transport = Mock()
|
||||
|
||||
original_send_tts = satellite._send_tts
|
||||
|
||||
async def send_tts(*args, **kwargs):
|
||||
@@ -656,6 +663,8 @@ async def test_tts_wrong_wav_format(
|
||||
|
||||
satellite._send_tts = AsyncMock(side_effect=send_tts) # type: ignore[method-assign]
|
||||
|
||||
satellite.connection_made(Mock())
|
||||
|
||||
# silence
|
||||
satellite.on_chunk(bytes(_ONE_SECOND))
|
||||
|
||||
@@ -663,10 +672,18 @@ async def test_tts_wrong_wav_format(
|
||||
satellite.on_chunk(bytes([255] * _ONE_SECOND * 2))
|
||||
|
||||
# silence (assumes relaxed VAD sensitivity)
|
||||
satellite.on_chunk(bytes(_ONE_SECOND * 4))
|
||||
satellite.on_chunk(bytes(_ONE_SECOND))
|
||||
await asyncio.sleep(0.2)
|
||||
satellite.on_chunk(bytes(_ONE_SECOND))
|
||||
await asyncio.sleep(0.2)
|
||||
satellite.on_chunk(bytes(_ONE_SECOND))
|
||||
await asyncio.sleep(0.2)
|
||||
satellite.on_chunk(bytes(_ONE_SECOND))
|
||||
await asyncio.sleep(0.2)
|
||||
satellite.on_chunk(bytes(_ONE_SECOND))
|
||||
|
||||
# Wait for mock pipeline to exhaust the audio stream
|
||||
async with asyncio.timeout(1):
|
||||
async with asyncio.timeout(3):
|
||||
await done.wait()
|
||||
|
||||
|
||||
@@ -679,6 +696,7 @@ async def test_empty_tts_output(
|
||||
assert await async_setup_component(hass, "voip", {})
|
||||
|
||||
satellite = async_get_satellite_entity(hass, voip.DOMAIN, voip_device.voip_id)
|
||||
satellite.addr = ("192.168.1.1", 12345)
|
||||
assert isinstance(satellite, VoipAssistSatellite)
|
||||
|
||||
async def async_pipeline_from_audio_stream(*args, **kwargs):
|
||||
@@ -728,7 +746,7 @@ async def test_empty_tts_output(
|
||||
"homeassistant.components.voip.assist_satellite.VoipAssistSatellite._send_tts",
|
||||
) as mock_send_tts,
|
||||
):
|
||||
satellite.transport = Mock()
|
||||
satellite.connection_made(Mock())
|
||||
|
||||
# silence
|
||||
satellite.on_chunk(bytes(_ONE_SECOND))
|
||||
@@ -737,10 +755,18 @@ async def test_empty_tts_output(
|
||||
satellite.on_chunk(bytes([255] * _ONE_SECOND * 2))
|
||||
|
||||
# silence (assumes relaxed VAD sensitivity)
|
||||
satellite.on_chunk(bytes(_ONE_SECOND * 4))
|
||||
satellite.on_chunk(bytes(_ONE_SECOND))
|
||||
await asyncio.sleep(0.2)
|
||||
satellite.on_chunk(bytes(_ONE_SECOND))
|
||||
await asyncio.sleep(0.2)
|
||||
satellite.on_chunk(bytes(_ONE_SECOND))
|
||||
await asyncio.sleep(0.2)
|
||||
satellite.on_chunk(bytes(_ONE_SECOND))
|
||||
await asyncio.sleep(0.2)
|
||||
satellite.on_chunk(bytes(_ONE_SECOND))
|
||||
|
||||
# Wait for mock pipeline to finish
|
||||
async with asyncio.timeout(1):
|
||||
async with asyncio.timeout(2):
|
||||
await satellite._tts_done.wait()
|
||||
|
||||
mock_send_tts.assert_not_called()
|
||||
@@ -785,7 +811,7 @@ async def test_pipeline_error(
|
||||
),
|
||||
):
|
||||
satellite._tones = Tones.ERROR
|
||||
satellite.transport = Mock()
|
||||
satellite.connection_made(Mock())
|
||||
satellite._async_send_audio = AsyncMock(side_effect=async_send_audio) # type: ignore[method-assign]
|
||||
|
||||
satellite.on_chunk(bytes(_ONE_SECOND))
|
||||
@@ -845,16 +871,20 @@ async def test_announce(
|
||||
"homeassistant.components.voip.assist_satellite.VoipAssistSatellite._send_tts",
|
||||
) as mock_send_tts,
|
||||
):
|
||||
satellite.transport = Mock()
|
||||
announce_task = hass.async_create_background_task(
|
||||
satellite.async_announce(announcement), "voip_announce"
|
||||
)
|
||||
await asyncio.sleep(0)
|
||||
satellite.connection_made(Mock())
|
||||
mock_protocol.outgoing_call.assert_called_once()
|
||||
|
||||
# Trigger announcement
|
||||
satellite.on_chunk(bytes(_ONE_SECOND))
|
||||
async with asyncio.timeout(1):
|
||||
await asyncio.sleep(0.2)
|
||||
satellite.on_chunk(bytes(_ONE_SECOND))
|
||||
await asyncio.sleep(0.2)
|
||||
satellite.on_chunk(bytes(_ONE_SECOND))
|
||||
async with asyncio.timeout(2):
|
||||
await announce_task
|
||||
|
||||
mock_send_tts.assert_called_once_with(
|
||||
@@ -897,11 +927,11 @@ async def test_voip_id_is_ip_address(
|
||||
"homeassistant.components.voip.assist_satellite.VoipAssistSatellite._send_tts",
|
||||
) as mock_send_tts,
|
||||
):
|
||||
satellite.transport = Mock()
|
||||
announce_task = hass.async_create_background_task(
|
||||
satellite.async_announce(announcement), "voip_announce"
|
||||
)
|
||||
await asyncio.sleep(0)
|
||||
satellite.connection_made(Mock())
|
||||
mock_protocol.outgoing_call.assert_called_once()
|
||||
assert (
|
||||
mock_protocol.outgoing_call.call_args.kwargs["destination"].host
|
||||
@@ -910,7 +940,11 @@ async def test_voip_id_is_ip_address(
|
||||
|
||||
# Trigger announcement
|
||||
satellite.on_chunk(bytes(_ONE_SECOND))
|
||||
async with asyncio.timeout(1):
|
||||
await asyncio.sleep(0.2)
|
||||
satellite.on_chunk(bytes(_ONE_SECOND))
|
||||
await asyncio.sleep(0.2)
|
||||
satellite.on_chunk(bytes(_ONE_SECOND))
|
||||
async with asyncio.timeout(2):
|
||||
await announce_task
|
||||
|
||||
mock_send_tts.assert_called_once_with(
|
||||
@@ -955,7 +989,7 @@ async def test_announce_timeout(
|
||||
0.01,
|
||||
),
|
||||
):
|
||||
satellite.transport = Mock()
|
||||
satellite.connection_made(Mock())
|
||||
with pytest.raises(TimeoutError):
|
||||
await satellite.async_announce(announcement)
|
||||
|
||||
@@ -1042,7 +1076,7 @@ async def test_start_conversation(
|
||||
new=async_pipeline_from_audio_stream,
|
||||
),
|
||||
):
|
||||
satellite.transport = Mock()
|
||||
satellite.connection_made(Mock())
|
||||
conversation_task = hass.async_create_background_task(
|
||||
satellite.async_start_conversation(announcement), "voip_start_conversation"
|
||||
)
|
||||
@@ -1051,16 +1085,20 @@ async def test_start_conversation(
|
||||
|
||||
# Trigger announcement and wait for it to finish
|
||||
satellite.on_chunk(bytes(_ONE_SECOND))
|
||||
async with asyncio.timeout(1):
|
||||
await asyncio.sleep(0.2)
|
||||
satellite.on_chunk(bytes(_ONE_SECOND))
|
||||
await asyncio.sleep(0.2)
|
||||
satellite.on_chunk(bytes(_ONE_SECOND))
|
||||
async with asyncio.timeout(2):
|
||||
await tts_sent.wait()
|
||||
|
||||
tts_sent.clear()
|
||||
|
||||
# Trigger pipeline
|
||||
satellite.on_chunk(bytes(_ONE_SECOND))
|
||||
async with asyncio.timeout(1):
|
||||
# Wait for TTS
|
||||
await tts_sent.wait()
|
||||
await asyncio.sleep(0.2)
|
||||
satellite.on_chunk(bytes(_ONE_SECOND))
|
||||
await asyncio.sleep(3)
|
||||
async with asyncio.timeout(3):
|
||||
# Wait for Conversation end
|
||||
await conversation_task
|
||||
|
||||
|
||||
@@ -1073,21 +1111,8 @@ async def test_start_conversation_user_doesnt_pick_up(
|
||||
"""Test start conversation when the user doesn't pick up."""
|
||||
assert await async_setup_component(hass, "voip", {})
|
||||
|
||||
pipeline = assist_pipeline.Pipeline(
|
||||
conversation_engine="test engine",
|
||||
conversation_language="en",
|
||||
language="en",
|
||||
name="test pipeline",
|
||||
stt_engine="test stt",
|
||||
stt_language="en",
|
||||
tts_engine="test tts",
|
||||
tts_language="en",
|
||||
tts_voice=None,
|
||||
wake_word_entity=None,
|
||||
wake_word_id=None,
|
||||
)
|
||||
|
||||
satellite = async_get_satellite_entity(hass, voip.DOMAIN, voip_device.voip_id)
|
||||
satellite.addr = ("192.168.1.1", 12345)
|
||||
assert isinstance(satellite, VoipAssistSatellite)
|
||||
assert (
|
||||
satellite.supported_features
|
||||
@@ -1098,62 +1123,22 @@ async def test_start_conversation_user_doesnt_pick_up(
|
||||
mock_protocol: AsyncMock = hass.data[DOMAIN].protocol
|
||||
mock_protocol.outgoing_call = Mock()
|
||||
|
||||
pipeline_started = asyncio.Event()
|
||||
|
||||
async def async_pipeline_from_audio_stream(
|
||||
hass: HomeAssistant,
|
||||
context: Context,
|
||||
*args,
|
||||
conversation_extra_system_prompt: str | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
# System prompt should be not be set due to timeout (user not picking up)
|
||||
assert conversation_extra_system_prompt is None
|
||||
|
||||
pipeline_started.set()
|
||||
announcement = assist_satellite.AssistSatelliteAnnouncement(
|
||||
message="test announcement",
|
||||
media_id=_MEDIA_ID,
|
||||
tts_token="test-token",
|
||||
original_media_id=_MEDIA_ID,
|
||||
media_id_source="tts",
|
||||
)
|
||||
|
||||
# Very short timeout which will trigger because we don't send any audio in
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.assist_satellite.entity.async_get_pipeline",
|
||||
return_value=pipeline,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.voip.assist_satellite.VoipAssistSatellite.async_start_conversation",
|
||||
side_effect=TimeoutError,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.assist_satellite.entity.async_pipeline_from_audio_stream",
|
||||
new=async_pipeline_from_audio_stream,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.tts.generate_media_source_id",
|
||||
return_value="media-source://bla",
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.tts.async_resolve_engine",
|
||||
return_value="test tts",
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.tts.async_create_stream",
|
||||
return_value=MockResultStream(hass, "wav", b""),
|
||||
"homeassistant.components.voip.assist_satellite._ANNOUNCEMENT_RING_TIMEOUT",
|
||||
0.1,
|
||||
),
|
||||
):
|
||||
satellite.transport = Mock()
|
||||
satellite.connection_made(Mock())
|
||||
|
||||
# Error should clear system prompt
|
||||
with pytest.raises(TimeoutError):
|
||||
await hass.services.async_call(
|
||||
assist_satellite.DOMAIN,
|
||||
"start_conversation",
|
||||
{
|
||||
"entity_id": satellite.entity_id,
|
||||
"start_message": "test announcement",
|
||||
"extra_system_prompt": "test prompt",
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# Trigger a pipeline so we can check if the system prompt was cleared
|
||||
satellite.on_chunk(bytes(_ONE_SECOND))
|
||||
async with asyncio.timeout(1):
|
||||
await pipeline_started.wait()
|
||||
await satellite.async_start_conversation(announcement)
|
||||
|
||||
@@ -190,6 +190,19 @@ def mock_sdk_version(client: MagicMock) -> Generator[None]:
|
||||
client.driver.controller.data["sdkVersion"] = original_sdk_version
|
||||
|
||||
|
||||
@pytest.fixture(name="driver_ready_timeout")
|
||||
def mock_driver_ready_timeout() -> Generator[None]:
|
||||
"""Mock migration nvm restore driver ready timeout."""
|
||||
with patch(
|
||||
(
|
||||
"homeassistant.components.zwave_js.config_flow."
|
||||
"RESTORE_NVM_DRIVER_READY_TIMEOUT"
|
||||
),
|
||||
new=0,
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
async def test_manual(hass: HomeAssistant) -> None:
|
||||
"""Test we create an entry with manual step."""
|
||||
|
||||
@@ -653,6 +666,7 @@ async def test_usb_discovery(
|
||||
install_addon,
|
||||
addon_options,
|
||||
get_addon_discovery_info,
|
||||
mock_usb_serial_by_id: MagicMock,
|
||||
set_addon_options,
|
||||
start_addon,
|
||||
usb_discovery_info: UsbServiceInfo,
|
||||
@@ -668,6 +682,7 @@ async def test_usb_discovery(
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "usb_confirm"
|
||||
assert result["description_placeholders"] == {"name": discovery_name}
|
||||
assert mock_usb_serial_by_id.call_count == 1
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
|
||||
@@ -682,7 +697,7 @@ async def test_usb_discovery(
|
||||
assert install_addon.call_args == call("core_zwave_js")
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "configure_addon"
|
||||
assert result["step_id"] == "configure_addon_user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@@ -765,6 +780,7 @@ async def test_usb_discovery_addon_not_running(
|
||||
supervisor,
|
||||
addon_installed,
|
||||
addon_options,
|
||||
mock_usb_serial_by_id: MagicMock,
|
||||
set_addon_options,
|
||||
start_addon,
|
||||
get_addon_discovery_info,
|
||||
@@ -779,11 +795,12 @@ async def test_usb_discovery_addon_not_running(
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "usb_confirm"
|
||||
assert mock_usb_serial_by_id.call_count == 2
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "configure_addon"
|
||||
assert result["step_id"] == "configure_addon_user"
|
||||
|
||||
# Make sure the discovered usb device is preferred.
|
||||
data_schema = result["data_schema"]
|
||||
@@ -876,6 +893,7 @@ async def test_usb_discovery_addon_not_running(
|
||||
async def test_usb_discovery_migration(
|
||||
hass: HomeAssistant,
|
||||
addon_options: dict[str, Any],
|
||||
mock_usb_serial_by_id: MagicMock,
|
||||
set_addon_options: AsyncMock,
|
||||
restart_addon: AsyncMock,
|
||||
client: MagicMock,
|
||||
@@ -884,6 +902,144 @@ async def test_usb_discovery_migration(
|
||||
"""Test usb discovery migration."""
|
||||
addon_options["device"] = "/dev/ttyUSB0"
|
||||
entry = integration
|
||||
assert client.connect.call_count == 1
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
unique_id="1234",
|
||||
data={
|
||||
"url": "ws://localhost:3000",
|
||||
"use_addon": True,
|
||||
"usb_path": "/dev/ttyUSB0",
|
||||
},
|
||||
)
|
||||
|
||||
async def mock_backup_nvm_raw():
|
||||
await asyncio.sleep(0)
|
||||
client.driver.controller.emit(
|
||||
"nvm backup progress", {"bytesRead": 100, "total": 200}
|
||||
)
|
||||
return b"test_nvm_data"
|
||||
|
||||
client.driver.controller.async_backup_nvm_raw = AsyncMock(
|
||||
side_effect=mock_backup_nvm_raw
|
||||
)
|
||||
|
||||
async def mock_restore_nvm(data: bytes):
|
||||
client.driver.controller.emit(
|
||||
"nvm convert progress",
|
||||
{"event": "nvm convert progress", "bytesRead": 100, "total": 200},
|
||||
)
|
||||
await asyncio.sleep(0)
|
||||
client.driver.controller.emit(
|
||||
"nvm restore progress",
|
||||
{"event": "nvm restore progress", "bytesWritten": 100, "total": 200},
|
||||
)
|
||||
client.driver.emit(
|
||||
"driver ready", {"event": "driver ready", "source": "driver"}
|
||||
)
|
||||
|
||||
client.driver.controller.async_restore_nvm = AsyncMock(side_effect=mock_restore_nvm)
|
||||
|
||||
events = async_capture_events(
|
||||
hass, data_entry_flow.EVENT_DATA_ENTRY_FLOW_PROGRESS_UPDATE
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_USB},
|
||||
data=USB_DISCOVERY_INFO,
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "usb_confirm"
|
||||
assert mock_usb_serial_by_id.call_count == 2
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "intent_migrate"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
|
||||
assert result["type"] == FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "backup_nvm"
|
||||
|
||||
with patch("pathlib.Path.write_bytes", MagicMock()) as mock_file:
|
||||
await hass.async_block_till_done()
|
||||
assert client.driver.controller.async_backup_nvm_raw.call_count == 1
|
||||
assert mock_file.call_count == 1
|
||||
assert len(events) == 1
|
||||
assert events[0].data["progress"] == 0.5
|
||||
events.clear()
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "instruct_unplug"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
|
||||
assert entry.state is config_entries.ConfigEntryState.NOT_LOADED
|
||||
assert result["type"] == FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "start_addon"
|
||||
assert set_addon_options.call_args == call(
|
||||
"core_zwave_js", AddonsOptions(config={"device": USB_DISCOVERY_INFO.device})
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert restart_addon.call_args == call("core_zwave_js")
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||
|
||||
assert result["type"] == FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "restore_nvm"
|
||||
assert client.connect.call_count == 2
|
||||
|
||||
await hass.async_block_till_done()
|
||||
assert client.connect.call_count == 3
|
||||
assert entry.state is config_entries.ConfigEntryState.LOADED
|
||||
assert client.driver.controller.async_restore_nvm.call_count == 1
|
||||
assert len(events) == 2
|
||||
assert events[0].data["progress"] == 0.25
|
||||
assert events[1].data["progress"] == 0.75
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "migration_successful"
|
||||
assert integration.data["url"] == "ws://host1:3001"
|
||||
assert integration.data["usb_path"] == USB_DISCOVERY_INFO.device
|
||||
assert integration.data["use_addon"] is True
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("supervisor", "addon_running", "get_addon_discovery_info")
|
||||
@pytest.mark.parametrize(
|
||||
"discovery_info",
|
||||
[
|
||||
[
|
||||
Discovery(
|
||||
addon="core_zwave_js",
|
||||
service="zwave_js",
|
||||
uuid=uuid4(),
|
||||
config=ADDON_DISCOVERY_INFO,
|
||||
)
|
||||
]
|
||||
],
|
||||
)
|
||||
async def test_usb_discovery_migration_driver_ready_timeout(
|
||||
hass: HomeAssistant,
|
||||
addon_options: dict[str, Any],
|
||||
driver_ready_timeout: None,
|
||||
mock_usb_serial_by_id: MagicMock,
|
||||
set_addon_options: AsyncMock,
|
||||
restart_addon: AsyncMock,
|
||||
client: MagicMock,
|
||||
integration: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test driver ready timeout after nvm restore during usb discovery migration."""
|
||||
addon_options["device"] = "/dev/ttyUSB0"
|
||||
entry = integration
|
||||
assert client.connect.call_count == 1
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
unique_id="1234",
|
||||
@@ -929,6 +1085,7 @@ async def test_usb_discovery_migration(
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "usb_confirm"
|
||||
assert mock_usb_serial_by_id.call_count == 2
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
|
||||
@@ -952,10 +1109,10 @@ async def test_usb_discovery_migration(
|
||||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "instruct_unplug"
|
||||
assert entry.state is config_entries.ConfigEntryState.NOT_LOADED
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
|
||||
assert entry.state is config_entries.ConfigEntryState.NOT_LOADED
|
||||
assert result["type"] == FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "start_addon"
|
||||
assert set_addon_options.call_args == call(
|
||||
@@ -970,8 +1127,10 @@ async def test_usb_discovery_migration(
|
||||
|
||||
assert result["type"] == FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "restore_nvm"
|
||||
assert client.connect.call_count == 2
|
||||
|
||||
await hass.async_block_till_done()
|
||||
assert client.connect.call_count == 3
|
||||
assert entry.state is config_entries.ConfigEntryState.LOADED
|
||||
assert client.driver.controller.async_restore_nvm.call_count == 1
|
||||
assert len(events) == 2
|
||||
@@ -1015,7 +1174,7 @@ async def test_discovery_addon_not_running(
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "configure_addon"
|
||||
assert result["step_id"] == "configure_addon_user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@@ -1117,7 +1276,7 @@ async def test_discovery_addon_not_installed(
|
||||
assert install_addon.call_args == call("core_zwave_js")
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "configure_addon"
|
||||
assert result["step_id"] == "configure_addon_user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@@ -1278,6 +1437,7 @@ async def test_abort_usb_discovery_addon_required(
|
||||
async def test_abort_usb_discovery_confirm_addon_required(
|
||||
hass: HomeAssistant,
|
||||
addon_options: dict[str, Any],
|
||||
mock_usb_serial_by_id: MagicMock,
|
||||
) -> None:
|
||||
"""Test usb discovery confirm aborted when existing entry not using add-on."""
|
||||
addon_options["device"] = "/dev/another_device"
|
||||
@@ -1301,6 +1461,7 @@ async def test_abort_usb_discovery_confirm_addon_required(
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "usb_confirm"
|
||||
assert mock_usb_serial_by_id.call_count == 2
|
||||
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
@@ -1331,6 +1492,7 @@ async def test_usb_discovery_requires_supervisor(hass: HomeAssistant) -> None:
|
||||
async def test_usb_discovery_same_device(
|
||||
hass: HomeAssistant,
|
||||
addon_options: dict[str, Any],
|
||||
mock_usb_serial_by_id: MagicMock,
|
||||
) -> None:
|
||||
"""Test usb discovery flow is aborted when the add-on device is discovered."""
|
||||
addon_options["device"] = USB_DISCOVERY_INFO.device
|
||||
@@ -1341,6 +1503,7 @@ async def test_usb_discovery_same_device(
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
assert mock_usb_serial_by_id.call_count == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -1674,7 +1837,7 @@ async def test_addon_installed(
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "configure_addon"
|
||||
assert result["step_id"] == "configure_addon_user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@@ -1777,7 +1940,7 @@ async def test_addon_installed_start_failure(
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "configure_addon"
|
||||
assert result["step_id"] == "configure_addon_user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@@ -1862,7 +2025,7 @@ async def test_addon_installed_failures(
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "configure_addon"
|
||||
assert result["step_id"] == "configure_addon_user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@@ -1943,7 +2106,7 @@ async def test_addon_installed_set_options_failure(
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "configure_addon"
|
||||
assert result["step_id"] == "configure_addon_user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@@ -2058,7 +2221,7 @@ async def test_addon_installed_already_configured(
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "configure_addon"
|
||||
assert result["step_id"] == "configure_addon_user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@@ -2154,7 +2317,7 @@ async def test_addon_not_installed(
|
||||
assert install_addon.call_args == call("core_zwave_js")
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "configure_addon"
|
||||
assert result["step_id"] == "configure_addon_user"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@@ -2600,7 +2763,7 @@ async def test_reconfigure_addon_running(
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "configure_addon"
|
||||
assert result["step_id"] == "configure_addon_reconfigure"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@@ -2735,7 +2898,7 @@ async def test_reconfigure_addon_running_no_changes(
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "configure_addon"
|
||||
assert result["step_id"] == "configure_addon_reconfigure"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@@ -2916,7 +3079,7 @@ async def test_reconfigure_different_device(
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "configure_addon"
|
||||
assert result["step_id"] == "configure_addon_reconfigure"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@@ -3099,7 +3262,7 @@ async def test_reconfigure_addon_restart_failed(
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "configure_addon"
|
||||
assert result["step_id"] == "configure_addon_reconfigure"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@@ -3240,7 +3403,7 @@ async def test_reconfigure_addon_running_server_info_failure(
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "configure_addon"
|
||||
assert result["step_id"] == "configure_addon_reconfigure"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@@ -3387,7 +3550,7 @@ async def test_reconfigure_addon_not_installed(
|
||||
assert install_addon.call_args == call("core_zwave_js")
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "configure_addon"
|
||||
assert result["step_id"] == "configure_addon_reconfigure"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@@ -3542,6 +3705,152 @@ async def test_reconfigure_migrate_with_addon(
|
||||
) -> None:
|
||||
"""Test migration flow with add-on."""
|
||||
entry = integration
|
||||
assert client.connect.call_count == 1
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
unique_id="1234",
|
||||
data={
|
||||
"url": "ws://localhost:3000",
|
||||
"use_addon": True,
|
||||
"usb_path": "/dev/ttyUSB0",
|
||||
},
|
||||
)
|
||||
|
||||
async def mock_backup_nvm_raw():
|
||||
await asyncio.sleep(0)
|
||||
client.driver.controller.emit(
|
||||
"nvm backup progress", {"bytesRead": 100, "total": 200}
|
||||
)
|
||||
return b"test_nvm_data"
|
||||
|
||||
client.driver.controller.async_backup_nvm_raw = AsyncMock(
|
||||
side_effect=mock_backup_nvm_raw
|
||||
)
|
||||
|
||||
async def mock_restore_nvm(data: bytes):
|
||||
client.driver.controller.emit(
|
||||
"nvm convert progress",
|
||||
{"event": "nvm convert progress", "bytesRead": 100, "total": 200},
|
||||
)
|
||||
await asyncio.sleep(0)
|
||||
client.driver.controller.emit(
|
||||
"nvm restore progress",
|
||||
{"event": "nvm restore progress", "bytesWritten": 100, "total": 200},
|
||||
)
|
||||
client.driver.emit(
|
||||
"driver ready", {"event": "driver ready", "source": "driver"}
|
||||
)
|
||||
|
||||
client.driver.controller.async_restore_nvm = AsyncMock(side_effect=mock_restore_nvm)
|
||||
|
||||
events = async_capture_events(
|
||||
hass, data_entry_flow.EVENT_DATA_ENTRY_FLOW_PROGRESS_UPDATE
|
||||
)
|
||||
|
||||
result = await entry.start_reconfigure_flow(hass)
|
||||
|
||||
assert result["type"] == FlowResultType.MENU
|
||||
assert result["step_id"] == "reconfigure"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {"next_step_id": "intent_migrate"}
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "intent_migrate"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
|
||||
assert result["type"] == FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "backup_nvm"
|
||||
|
||||
with patch("pathlib.Path.write_bytes", MagicMock()) as mock_file:
|
||||
await hass.async_block_till_done()
|
||||
assert client.driver.controller.async_backup_nvm_raw.call_count == 1
|
||||
assert mock_file.call_count == 1
|
||||
assert len(events) == 1
|
||||
assert events[0].data["progress"] == 0.5
|
||||
events.clear()
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "instruct_unplug"
|
||||
assert entry.state is config_entries.ConfigEntryState.NOT_LOADED
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "choose_serial_port"
|
||||
assert result["data_schema"].schema[CONF_USB_PATH]
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_USB_PATH: "/test",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "start_addon"
|
||||
assert set_addon_options.call_args == call(
|
||||
"core_zwave_js", AddonsOptions(config={"device": "/test"})
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert restart_addon.call_args == call("core_zwave_js")
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||
|
||||
assert result["type"] == FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "restore_nvm"
|
||||
assert client.connect.call_count == 2
|
||||
|
||||
await hass.async_block_till_done()
|
||||
assert client.connect.call_count == 3
|
||||
assert entry.state is config_entries.ConfigEntryState.LOADED
|
||||
assert client.driver.controller.async_restore_nvm.call_count == 1
|
||||
assert len(events) == 2
|
||||
assert events[0].data["progress"] == 0.25
|
||||
assert events[1].data["progress"] == 0.75
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "migration_successful"
|
||||
assert integration.data["url"] == "ws://host1:3001"
|
||||
assert integration.data["usb_path"] == "/test"
|
||||
assert integration.data["use_addon"] is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"discovery_info",
|
||||
[
|
||||
[
|
||||
Discovery(
|
||||
addon="core_zwave_js",
|
||||
service="zwave_js",
|
||||
uuid=uuid4(),
|
||||
config=ADDON_DISCOVERY_INFO,
|
||||
)
|
||||
]
|
||||
],
|
||||
)
|
||||
async def test_reconfigure_migrate_driver_ready_timeout(
|
||||
hass: HomeAssistant,
|
||||
client,
|
||||
supervisor,
|
||||
integration,
|
||||
addon_running,
|
||||
driver_ready_timeout: None,
|
||||
restart_addon,
|
||||
set_addon_options,
|
||||
get_addon_discovery_info,
|
||||
) -> None:
|
||||
"""Test migration flow with driver ready timeout after nvm restore."""
|
||||
entry = integration
|
||||
assert client.connect.call_count == 1
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
unique_id="1234",
|
||||
@@ -3609,6 +3918,7 @@ async def test_reconfigure_migrate_with_addon(
|
||||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "instruct_unplug"
|
||||
assert entry.state is config_entries.ConfigEntryState.NOT_LOADED
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
|
||||
@@ -3623,7 +3933,6 @@ async def test_reconfigure_migrate_with_addon(
|
||||
},
|
||||
)
|
||||
|
||||
assert entry.state is config_entries.ConfigEntryState.NOT_LOADED
|
||||
assert result["type"] == FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "start_addon"
|
||||
assert set_addon_options.call_args == call(
|
||||
@@ -3638,8 +3947,10 @@ async def test_reconfigure_migrate_with_addon(
|
||||
|
||||
assert result["type"] == FlowResultType.SHOW_PROGRESS
|
||||
assert result["step_id"] == "restore_nvm"
|
||||
assert client.connect.call_count == 2
|
||||
|
||||
await hass.async_block_till_done()
|
||||
assert client.connect.call_count == 3
|
||||
assert entry.state is config_entries.ConfigEntryState.LOADED
|
||||
assert client.driver.controller.async_restore_nvm.call_count == 1
|
||||
assert len(events) == 2
|
||||
@@ -3797,6 +4108,7 @@ async def test_reconfigure_migrate_start_addon_failure(
|
||||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "instruct_unplug"
|
||||
assert entry.state is config_entries.ConfigEntryState.NOT_LOADED
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
|
||||
@@ -3891,6 +4203,7 @@ async def test_reconfigure_migrate_restore_failure(
|
||||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "instruct_unplug"
|
||||
assert entry.state is config_entries.ConfigEntryState.NOT_LOADED
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
|
||||
@@ -4056,6 +4369,7 @@ async def test_choose_serial_port_usb_ports_failure(
|
||||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "instruct_unplug"
|
||||
assert entry.state is config_entries.ConfigEntryState.NOT_LOADED
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.zwave_js.config_flow.async_get_usb_ports",
|
||||
|
||||
@@ -110,6 +110,10 @@ class AiohttpClientMocker:
|
||||
"""Register a mock patch request."""
|
||||
self.request("patch", *args, **kwargs)
|
||||
|
||||
def head(self, *args, **kwargs):
|
||||
"""Register a mock head request."""
|
||||
self.request("head", *args, **kwargs)
|
||||
|
||||
@property
|
||||
def call_count(self):
|
||||
"""Return the number of requests made."""
|
||||
|
||||
Reference in New Issue
Block a user