forked from home-assistant/core
Compare commits
223 Commits
whirlpool_
...
2025.5.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e6473d130 | ||
|
|
9a183bc16a | ||
|
|
e540247c14 | ||
|
|
0aef8b58d8 | ||
|
|
f0501f917b | ||
|
|
97004e13cb | ||
|
|
f867a0af24 | ||
|
|
d3b3839ffa | ||
|
|
1a227d6a10 | ||
|
|
fc8c403a3a | ||
|
|
c1bf596eba | ||
|
|
63f69a9e3d | ||
|
|
e13b014b6f | ||
|
|
be0d4d926c | ||
|
|
2403fff81f | ||
|
|
8c475787cc | ||
|
|
d9fe1edd82 | ||
|
|
f5cf64700a | ||
|
|
777b04d7a5 | ||
|
|
9fc78ed4e2 | ||
|
|
d03af549d4 | ||
|
|
d91f01243c | ||
|
|
5094208db6 | ||
|
|
006f66a841 | ||
|
|
64b7d77840 | ||
|
|
abf6a809b8 | ||
|
|
1b7dd205c7 | ||
|
|
3e00366a61 | ||
|
|
a17275b559 | ||
|
|
9534a919ce | ||
|
|
422dbfef88 | ||
|
|
8e44684a61 | ||
|
|
642e7fd487 | ||
|
|
9bb9132e7b | ||
|
|
41be82f167 | ||
|
|
47140e14d9 | ||
|
|
926502b0f1 | ||
|
|
78351ff7a7 | ||
|
|
c333726867 | ||
|
|
f66feabaaf | ||
|
|
0ef098a9f3 | ||
|
|
02b028add3 | ||
|
|
34455f9743 | ||
|
|
8c4eec231f | ||
|
|
621a14d7cc | ||
|
|
4906e78a5c | ||
|
|
146e440d59 | ||
|
|
e2ede3ed19 | ||
|
|
b76ac68fb1 | ||
|
|
0691ad9362 | ||
|
|
715f116954 | ||
|
|
9f0db98745 | ||
|
|
0ba55c31e8 | ||
|
|
19b7cfbd4a | ||
|
|
a9520888cf | ||
|
|
f086f4a955 | ||
|
|
a657964c25 | ||
|
|
543104b36c | ||
|
|
bf1d2069e4 | ||
|
|
e5e1c9fb05 | ||
|
|
4c4be88323 | ||
|
|
5a83627dc5 | ||
|
|
3123a7b168 | ||
|
|
8161ce6ea8 | ||
|
|
d9cbd1b65f | ||
|
|
b7c07209b8 | ||
|
|
6c3a4f17f0 | ||
|
|
d82feb807f | ||
|
|
c373fa9296 | ||
|
|
139b48440f | ||
|
|
9de1d3b143 | ||
|
|
b69ebdaecb | ||
|
|
f25e50b017 | ||
|
|
a4a7601f9f | ||
|
|
41a503f76f | ||
|
|
f1a3d62db2 | ||
|
|
e465276464 | ||
|
|
47b45444eb | ||
|
|
cf0911cc56 | ||
|
|
da79d5b2e3 | ||
|
|
358b0c1c17 | ||
|
|
543348fe58 | ||
|
|
0635856761 | ||
|
|
081afe6034 | ||
|
|
ca14322227 | ||
|
|
a54816a6e5 | ||
|
|
27db4e90b5 | ||
|
|
e9cc624d93 | ||
|
|
5a95f43992 | ||
|
|
36a35132c0 | ||
|
|
2fbc75f89b | ||
|
|
48aa6be889 | ||
|
|
bde04bc47b | ||
|
|
7d163aa659 | ||
|
|
010b044379 | ||
|
|
00627b82e0 | ||
|
|
13aba6201e | ||
|
|
f392e0c1c7 | ||
|
|
181eca6c82 | ||
|
|
196d923ac6 | ||
|
|
4ad387c967 | ||
|
|
cb475bf153 | ||
|
|
47acceea08 | ||
|
|
fd6fb7e3bc | ||
|
|
30f7e9b441 | ||
|
|
a8beec2691 | ||
|
|
23244fb79f | ||
|
|
e5c56629e2 | ||
|
|
a793503c8a | ||
|
|
054c7a0adc | ||
|
|
6eb2d1aa7c | ||
|
|
619fdea5df | ||
|
|
e8bdc7286e | ||
|
|
18f2b120ef | ||
|
|
43d8345821 | ||
|
|
999e930fc8 | ||
|
|
d4e99efc46 | ||
|
|
fb01a0a9f1 | ||
|
|
9556285c59 | ||
|
|
2d40b1ec75 | ||
|
|
7eb690b125 | ||
|
|
a23644debc | ||
|
|
c98ba7f6ba | ||
|
|
aa2b61f133 | ||
|
|
f85d4afe45 | ||
|
|
b4ab9177b8 | ||
|
|
e7c310ca58 | ||
|
|
85a83f2553 | ||
|
|
d2e7baeb38 | ||
|
|
07b2ce28b1 | ||
|
|
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 | ||
|
|
16f36912db | ||
|
|
2a5f031ba5 | ||
|
|
71bb8ae529 | ||
|
|
f6a94d0661 | ||
|
|
c2575735ff | ||
|
|
7eee5ecd9a | ||
|
|
db0cf9fbf4 | ||
|
|
aded44ee0f | ||
|
|
901926e8e6 | ||
|
|
2e336626ac | ||
|
|
3ea3d77f4d | ||
|
|
fe8e7b73bf | ||
|
|
a34065ee2f | ||
|
|
e1a908c8ac | ||
|
|
628d99886a | ||
|
|
6b10710484 | ||
|
|
8a4f28fa94 | ||
|
|
e7331633c7 | ||
|
|
934be08a59 | ||
|
|
485522fd76 | ||
|
|
eba0daa2e9 | ||
|
|
851779e7ad | ||
|
|
ea9a0f4bf5 | ||
|
|
1143468eb5 | ||
|
|
52b0b1e2ab | ||
|
|
23ba652b83 | ||
|
|
43b737c4a2 | ||
|
|
3fbd23b98d | ||
|
|
0cbeeebd0b | ||
|
|
99a0679ee9 | ||
|
|
e82713b68c | ||
|
|
9293afd95a | ||
|
|
9ea3e786f6 | ||
|
|
a8169d2056 | ||
|
|
1cd94affd1 | ||
|
|
724825d34c |
8
CODEOWNERS
generated
8
CODEOWNERS
generated
@@ -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
|
||||
|
||||
10
build.yaml
10
build.yaml
@@ -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%]",
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyaprilaire"],
|
||||
"requirements": ["pyaprilaire==0.8.1"]
|
||||
"requirements": ["pyaprilaire==0.9.0"]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""The S3 integration."""
|
||||
"""The AWS S3 integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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"],
|
||||
@@ -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%]"
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
from aiohttp import ClientTimeout
|
||||
from azure.core.exceptions import (
|
||||
AzureError,
|
||||
ClientAuthenticationError,
|
||||
HttpResponseError,
|
||||
ResourceNotFoundError,
|
||||
)
|
||||
from azure.core.pipeline.transport._aiohttp import (
|
||||
@@ -39,11 +39,20 @@ async def async_setup_entry(
|
||||
session = async_create_clientsession(
|
||||
hass, timeout=ClientTimeout(connect=10, total=12 * 60 * 60)
|
||||
)
|
||||
container_client = ContainerClient(
|
||||
account_url=f"https://{entry.data[CONF_ACCOUNT_NAME]}.blob.core.windows.net/",
|
||||
container_name=entry.data[CONF_CONTAINER_NAME],
|
||||
credential=entry.data[CONF_STORAGE_ACCOUNT_KEY],
|
||||
transport=AioHttpTransport(session=session),
|
||||
|
||||
def create_container_client() -> ContainerClient:
|
||||
"""Create a ContainerClient."""
|
||||
|
||||
return ContainerClient(
|
||||
account_url=f"https://{entry.data[CONF_ACCOUNT_NAME]}.blob.core.windows.net/",
|
||||
container_name=entry.data[CONF_CONTAINER_NAME],
|
||||
credential=entry.data[CONF_STORAGE_ACCOUNT_KEY],
|
||||
transport=AioHttpTransport(session=session),
|
||||
)
|
||||
|
||||
# has a blocking call to open in cpython
|
||||
container_client: ContainerClient = await hass.async_add_executor_job(
|
||||
create_container_client
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -61,7 +70,7 @@ async def async_setup_entry(
|
||||
translation_key="invalid_auth",
|
||||
translation_placeholders={CONF_ACCOUNT_NAME: entry.data[CONF_ACCOUNT_NAME]},
|
||||
) from err
|
||||
except HttpResponseError as err:
|
||||
except AzureError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
|
||||
@@ -8,7 +8,7 @@ import json
|
||||
import logging
|
||||
from typing import Any, Concatenate
|
||||
|
||||
from azure.core.exceptions import HttpResponseError
|
||||
from azure.core.exceptions import AzureError, HttpResponseError, ServiceRequestError
|
||||
from azure.storage.blob import BlobProperties
|
||||
|
||||
from homeassistant.components.backup import (
|
||||
@@ -80,6 +80,20 @@ def handle_backup_errors[_R, **P](
|
||||
f"Error during backup operation in {func.__name__}:"
|
||||
f" Status {err.status_code}, message: {err.message}"
|
||||
) from err
|
||||
except ServiceRequestError as err:
|
||||
raise BackupAgentError(
|
||||
f"Timeout during backup operation in {func.__name__}"
|
||||
) from err
|
||||
except AzureError as err:
|
||||
_LOGGER.debug(
|
||||
"Error during backup in %s: %s",
|
||||
func.__name__,
|
||||
err,
|
||||
exc_info=True,
|
||||
)
|
||||
raise BackupAgentError(
|
||||
f"Error during backup operation in {func.__name__}: {err}"
|
||||
) from err
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@@ -27,9 +27,25 @@ _LOGGER = logging.getLogger(__name__)
|
||||
class AzureStorageConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for azure storage."""
|
||||
|
||||
def get_account_url(self, account_name: str) -> str:
|
||||
"""Get the account URL."""
|
||||
return f"https://{account_name}.blob.core.windows.net/"
|
||||
async def get_container_client(
|
||||
self, account_name: str, container_name: str, storage_account_key: str
|
||||
) -> ContainerClient:
|
||||
"""Get the container client.
|
||||
|
||||
ContainerClient has a blocking call to open in cpython
|
||||
"""
|
||||
|
||||
session = async_get_clientsession(self.hass)
|
||||
|
||||
def create_container_client() -> ContainerClient:
|
||||
return ContainerClient(
|
||||
account_url=f"https://{account_name}.blob.core.windows.net/",
|
||||
container_name=container_name,
|
||||
credential=storage_account_key,
|
||||
transport=AioHttpTransport(session=session),
|
||||
)
|
||||
|
||||
return await self.hass.async_add_executor_job(create_container_client)
|
||||
|
||||
async def validate_config(
|
||||
self, container_client: ContainerClient
|
||||
@@ -58,11 +74,10 @@ class AzureStorageConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self._async_abort_entries_match(
|
||||
{CONF_ACCOUNT_NAME: user_input[CONF_ACCOUNT_NAME]}
|
||||
)
|
||||
container_client = ContainerClient(
|
||||
account_url=self.get_account_url(user_input[CONF_ACCOUNT_NAME]),
|
||||
container_client = await self.get_container_client(
|
||||
account_name=user_input[CONF_ACCOUNT_NAME],
|
||||
container_name=user_input[CONF_CONTAINER_NAME],
|
||||
credential=user_input[CONF_STORAGE_ACCOUNT_KEY],
|
||||
transport=AioHttpTransport(session=async_get_clientsession(self.hass)),
|
||||
storage_account_key=user_input[CONF_STORAGE_ACCOUNT_KEY],
|
||||
)
|
||||
errors = await self.validate_config(container_client)
|
||||
|
||||
@@ -99,12 +114,12 @@ class AzureStorageConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
reauth_entry = self._get_reauth_entry()
|
||||
|
||||
if user_input is not None:
|
||||
container_client = ContainerClient(
|
||||
account_url=self.get_account_url(reauth_entry.data[CONF_ACCOUNT_NAME]),
|
||||
container_client = await self.get_container_client(
|
||||
account_name=reauth_entry.data[CONF_ACCOUNT_NAME],
|
||||
container_name=reauth_entry.data[CONF_CONTAINER_NAME],
|
||||
credential=user_input[CONF_STORAGE_ACCOUNT_KEY],
|
||||
transport=AioHttpTransport(session=async_get_clientsession(self.hass)),
|
||||
storage_account_key=user_input[CONF_STORAGE_ACCOUNT_KEY],
|
||||
)
|
||||
|
||||
errors = await self.validate_config(container_client)
|
||||
if not errors:
|
||||
return self.async_update_reload_and_abort(
|
||||
@@ -129,13 +144,10 @@ class AzureStorageConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
reconfigure_entry = self._get_reconfigure_entry()
|
||||
|
||||
if user_input is not None:
|
||||
container_client = ContainerClient(
|
||||
account_url=self.get_account_url(
|
||||
reconfigure_entry.data[CONF_ACCOUNT_NAME]
|
||||
),
|
||||
container_client = await self.get_container_client(
|
||||
account_name=reconfigure_entry.data[CONF_ACCOUNT_NAME],
|
||||
container_name=user_input[CONF_CONTAINER_NAME],
|
||||
credential=user_input[CONF_STORAGE_ACCOUNT_KEY],
|
||||
transport=AioHttpTransport(session=async_get_clientsession(self.hass)),
|
||||
storage_account_key=user_input[CONF_STORAGE_ACCOUNT_KEY],
|
||||
)
|
||||
errors = await self.validate_config(container_client)
|
||||
if not errors:
|
||||
|
||||
@@ -202,7 +202,7 @@ class BackupConfig:
|
||||
if agent_id not in self.data.agents:
|
||||
old_agent_retention = None
|
||||
self.data.agents[agent_id] = AgentConfig(
|
||||
protected=agent_config.get("protected", False),
|
||||
protected=agent_config.get("protected", True),
|
||||
retention=new_agent_retention,
|
||||
)
|
||||
else:
|
||||
|
||||
@@ -22,7 +22,7 @@ from . import util
|
||||
from .agent import BackupAgent
|
||||
from .const import DATA_MANAGER
|
||||
from .manager import BackupManager
|
||||
from .models import BackupNotFound
|
||||
from .models import AgentBackup, BackupNotFound
|
||||
|
||||
|
||||
@callback
|
||||
@@ -85,7 +85,15 @@ class DownloadBackupView(HomeAssistantView):
|
||||
request, headers, backup_id, agent_id, agent, manager
|
||||
)
|
||||
return await self._send_backup_with_password(
|
||||
hass, request, headers, backup_id, agent_id, password, agent, manager
|
||||
hass,
|
||||
backup,
|
||||
request,
|
||||
headers,
|
||||
backup_id,
|
||||
agent_id,
|
||||
password,
|
||||
agent,
|
||||
manager,
|
||||
)
|
||||
except BackupNotFound:
|
||||
return Response(status=HTTPStatus.NOT_FOUND)
|
||||
@@ -116,6 +124,7 @@ class DownloadBackupView(HomeAssistantView):
|
||||
async def _send_backup_with_password(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
backup: AgentBackup,
|
||||
request: Request,
|
||||
headers: dict[istr, str],
|
||||
backup_id: str,
|
||||
@@ -144,7 +153,8 @@ class DownloadBackupView(HomeAssistantView):
|
||||
|
||||
stream = util.AsyncIteratorWriter(hass)
|
||||
worker = threading.Thread(
|
||||
target=util.decrypt_backup, args=[reader, stream, password, on_done, 0, []]
|
||||
target=util.decrypt_backup,
|
||||
args=[backup, reader, stream, password, on_done, 0, []],
|
||||
)
|
||||
try:
|
||||
worker.start()
|
||||
|
||||
@@ -295,13 +295,26 @@ def validate_password_stream(
|
||||
raise BackupEmpty
|
||||
|
||||
|
||||
def _get_expected_archives(backup: AgentBackup) -> set[str]:
|
||||
"""Get the expected archives in the backup."""
|
||||
expected_archives = set()
|
||||
if backup.homeassistant_included:
|
||||
expected_archives.add("homeassistant")
|
||||
for addon in backup.addons:
|
||||
expected_archives.add(addon.slug)
|
||||
for folder in backup.folders:
|
||||
expected_archives.add(folder.value)
|
||||
return expected_archives
|
||||
|
||||
|
||||
def decrypt_backup(
|
||||
backup: AgentBackup,
|
||||
input_stream: IO[bytes],
|
||||
output_stream: IO[bytes],
|
||||
password: str | None,
|
||||
on_done: Callable[[Exception | None], None],
|
||||
minimum_size: int,
|
||||
nonces: list[bytes],
|
||||
nonces: NonceGenerator,
|
||||
) -> None:
|
||||
"""Decrypt a backup."""
|
||||
error: Exception | None = None
|
||||
@@ -315,7 +328,7 @@ def decrypt_backup(
|
||||
fileobj=output_stream, mode="w|", bufsize=BUF_SIZE
|
||||
) as output_tar,
|
||||
):
|
||||
_decrypt_backup(input_tar, output_tar, password)
|
||||
_decrypt_backup(backup, input_tar, output_tar, password)
|
||||
except (DecryptError, SecureTarError, tarfile.TarError) as err:
|
||||
LOGGER.warning("Error decrypting backup: %s", err)
|
||||
error = err
|
||||
@@ -333,15 +346,18 @@ def decrypt_backup(
|
||||
|
||||
|
||||
def _decrypt_backup(
|
||||
backup: AgentBackup,
|
||||
input_tar: tarfile.TarFile,
|
||||
output_tar: tarfile.TarFile,
|
||||
password: str | None,
|
||||
) -> None:
|
||||
"""Decrypt a backup."""
|
||||
expected_archives = _get_expected_archives(backup)
|
||||
for obj in input_tar:
|
||||
# We compare with PurePath to avoid issues with different path separators,
|
||||
# for example when backup.json is added as "./backup.json"
|
||||
if PurePath(obj.name) == PurePath("backup.json"):
|
||||
object_path = PurePath(obj.name)
|
||||
if object_path == PurePath("backup.json"):
|
||||
# Rewrite the backup.json file to indicate that the backup is decrypted
|
||||
if not (reader := input_tar.extractfile(obj)):
|
||||
raise DecryptError
|
||||
@@ -352,7 +368,13 @@ def _decrypt_backup(
|
||||
metadata_obj.size = len(updated_metadata_b)
|
||||
output_tar.addfile(metadata_obj, BytesIO(updated_metadata_b))
|
||||
continue
|
||||
if not obj.name.endswith((".tar", ".tgz", ".tar.gz")):
|
||||
prefix, _, suffix = object_path.name.partition(".")
|
||||
if suffix not in ("tar", "tgz", "tar.gz"):
|
||||
LOGGER.debug("Unknown file %s will not be decrypted", obj.name)
|
||||
output_tar.addfile(obj, input_tar.extractfile(obj))
|
||||
continue
|
||||
if prefix not in expected_archives:
|
||||
LOGGER.debug("Unknown inner tar file %s will not be decrypted", obj.name)
|
||||
output_tar.addfile(obj, input_tar.extractfile(obj))
|
||||
continue
|
||||
istf = SecureTarFile(
|
||||
@@ -371,12 +393,13 @@ def _decrypt_backup(
|
||||
|
||||
|
||||
def encrypt_backup(
|
||||
backup: AgentBackup,
|
||||
input_stream: IO[bytes],
|
||||
output_stream: IO[bytes],
|
||||
password: str | None,
|
||||
on_done: Callable[[Exception | None], None],
|
||||
minimum_size: int,
|
||||
nonces: list[bytes],
|
||||
nonces: NonceGenerator,
|
||||
) -> None:
|
||||
"""Encrypt a backup."""
|
||||
error: Exception | None = None
|
||||
@@ -390,7 +413,7 @@ def encrypt_backup(
|
||||
fileobj=output_stream, mode="w|", bufsize=BUF_SIZE
|
||||
) as output_tar,
|
||||
):
|
||||
_encrypt_backup(input_tar, output_tar, password, nonces)
|
||||
_encrypt_backup(backup, input_tar, output_tar, password, nonces)
|
||||
except (EncryptError, SecureTarError, tarfile.TarError) as err:
|
||||
LOGGER.warning("Error encrypting backup: %s", err)
|
||||
error = err
|
||||
@@ -408,17 +431,20 @@ def encrypt_backup(
|
||||
|
||||
|
||||
def _encrypt_backup(
|
||||
backup: AgentBackup,
|
||||
input_tar: tarfile.TarFile,
|
||||
output_tar: tarfile.TarFile,
|
||||
password: str | None,
|
||||
nonces: list[bytes],
|
||||
nonces: NonceGenerator,
|
||||
) -> None:
|
||||
"""Encrypt a backup."""
|
||||
inner_tar_idx = 0
|
||||
expected_archives = _get_expected_archives(backup)
|
||||
for obj in input_tar:
|
||||
# We compare with PurePath to avoid issues with different path separators,
|
||||
# for example when backup.json is added as "./backup.json"
|
||||
if PurePath(obj.name) == PurePath("backup.json"):
|
||||
object_path = PurePath(obj.name)
|
||||
if object_path == PurePath("backup.json"):
|
||||
# Rewrite the backup.json file to indicate that the backup is encrypted
|
||||
if not (reader := input_tar.extractfile(obj)):
|
||||
raise EncryptError
|
||||
@@ -429,16 +455,21 @@ def _encrypt_backup(
|
||||
metadata_obj.size = len(updated_metadata_b)
|
||||
output_tar.addfile(metadata_obj, BytesIO(updated_metadata_b))
|
||||
continue
|
||||
if not obj.name.endswith((".tar", ".tgz", ".tar.gz")):
|
||||
prefix, _, suffix = object_path.name.partition(".")
|
||||
if suffix not in ("tar", "tgz", "tar.gz"):
|
||||
LOGGER.debug("Unknown file %s will not be encrypted", obj.name)
|
||||
output_tar.addfile(obj, input_tar.extractfile(obj))
|
||||
continue
|
||||
if prefix not in expected_archives:
|
||||
LOGGER.debug("Unknown inner tar file %s will not be encrypted", obj.name)
|
||||
continue
|
||||
istf = SecureTarFile(
|
||||
None, # Not used
|
||||
gzip=False,
|
||||
key=password_to_key(password) if password is not None else None,
|
||||
mode="r",
|
||||
fileobj=input_tar.extractfile(obj),
|
||||
nonce=nonces[inner_tar_idx],
|
||||
nonce=nonces.get(inner_tar_idx),
|
||||
)
|
||||
inner_tar_idx += 1
|
||||
with istf.encrypt(obj) as encrypted:
|
||||
@@ -456,17 +487,33 @@ class _CipherWorkerStatus:
|
||||
writer: AsyncIteratorWriter
|
||||
|
||||
|
||||
class NonceGenerator:
|
||||
"""Generate nonces for encryption."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the generator."""
|
||||
self._nonces: dict[int, bytes] = {}
|
||||
|
||||
def get(self, index: int) -> bytes:
|
||||
"""Get a nonce for the given index."""
|
||||
if index not in self._nonces:
|
||||
# Generate a new nonce for the given index
|
||||
self._nonces[index] = os.urandom(16)
|
||||
return self._nonces[index]
|
||||
|
||||
|
||||
class _CipherBackupStreamer:
|
||||
"""Encrypt or decrypt a backup."""
|
||||
|
||||
_cipher_func: Callable[
|
||||
[
|
||||
AgentBackup,
|
||||
IO[bytes],
|
||||
IO[bytes],
|
||||
str | None,
|
||||
Callable[[Exception | None], None],
|
||||
int,
|
||||
list[bytes],
|
||||
NonceGenerator,
|
||||
],
|
||||
None,
|
||||
]
|
||||
@@ -484,7 +531,7 @@ class _CipherBackupStreamer:
|
||||
self._hass = hass
|
||||
self._open_stream = open_stream
|
||||
self._password = password
|
||||
self._nonces: list[bytes] = []
|
||||
self._nonces = NonceGenerator()
|
||||
|
||||
def size(self) -> int:
|
||||
"""Return the maximum size of the decrypted or encrypted backup."""
|
||||
@@ -508,7 +555,15 @@ class _CipherBackupStreamer:
|
||||
writer = AsyncIteratorWriter(self._hass)
|
||||
worker = threading.Thread(
|
||||
target=self._cipher_func,
|
||||
args=[reader, writer, self._password, on_done, self.size(), self._nonces],
|
||||
args=[
|
||||
self._backup,
|
||||
reader,
|
||||
writer,
|
||||
self._password,
|
||||
on_done,
|
||||
self.size(),
|
||||
self._nonces,
|
||||
],
|
||||
)
|
||||
worker_status = _CipherWorkerStatus(
|
||||
done=asyncio.Event(), reader=reader, thread=worker, writer=writer
|
||||
@@ -538,17 +593,6 @@ class DecryptedBackupStreamer(_CipherBackupStreamer):
|
||||
class EncryptedBackupStreamer(_CipherBackupStreamer):
|
||||
"""Encrypt a backup."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
backup: AgentBackup,
|
||||
open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]],
|
||||
password: str | None,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(hass, backup, open_stream, password)
|
||||
self._nonces = [os.urandom(16) for _ in range(self._num_tar_files())]
|
||||
|
||||
_cipher_func = staticmethod(encrypt_backup)
|
||||
|
||||
def backup(self) -> AgentBackup:
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -18,9 +18,9 @@
|
||||
"bleak==0.22.3",
|
||||
"bleak-retry-connector==3.9.0",
|
||||
"bluetooth-adapters==0.21.4",
|
||||
"bluetooth-auto-recovery==1.4.5",
|
||||
"bluetooth-auto-recovery==1.5.1",
|
||||
"bluetooth-data-tools==1.28.1",
|
||||
"dbus-fast==2.43.0",
|
||||
"habluetooth==3.45.0"
|
||||
"habluetooth==3.48.2"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -418,9 +418,11 @@ class CloudTTSEntity(TextToSpeechEntity):
|
||||
language=language,
|
||||
voice=options.get(
|
||||
ATTR_VOICE,
|
||||
self._voice
|
||||
if language == self._language
|
||||
else DEFAULT_VOICES[language],
|
||||
(
|
||||
self._voice
|
||||
if language == self._language
|
||||
else DEFAULT_VOICES[language]
|
||||
),
|
||||
),
|
||||
gender=options.get(ATTR_GENDER),
|
||||
),
|
||||
@@ -435,6 +437,8 @@ class CloudTTSEntity(TextToSpeechEntity):
|
||||
class CloudProvider(Provider):
|
||||
"""Home Assistant Cloud speech API provider."""
|
||||
|
||||
has_entity = True
|
||||
|
||||
def __init__(self, cloud: Cloud[CloudClient]) -> None:
|
||||
"""Initialize cloud provider."""
|
||||
self.cloud = cloud
|
||||
|
||||
@@ -77,6 +77,5 @@ async def async_unload_entry(hass: HomeAssistant, entry: ComelitConfigEntry) ->
|
||||
coordinator = entry.runtime_data
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, platforms):
|
||||
await coordinator.api.logout()
|
||||
await coordinator.api.close()
|
||||
|
||||
return unload_ok
|
||||
|
||||
@@ -134,11 +134,9 @@ class ComelitClimateEntity(ComelitBridgeBaseEntity, ClimateEntity):
|
||||
self._attr_current_temperature = values[0] / 10
|
||||
|
||||
self._attr_hvac_action = None
|
||||
if _mode == ClimaComelitMode.OFF:
|
||||
self._attr_hvac_action = HVACAction.OFF
|
||||
if not _active:
|
||||
self._attr_hvac_action = HVACAction.IDLE
|
||||
if _mode in API_STATUS:
|
||||
elif _mode in API_STATUS:
|
||||
self._attr_hvac_action = API_STATUS[_mode]["hvac_action"]
|
||||
|
||||
self._attr_hvac_mode = None
|
||||
|
||||
@@ -73,7 +73,6 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
|
||||
) from err
|
||||
finally:
|
||||
await api.logout()
|
||||
await api.close()
|
||||
|
||||
return {"title": data[CONF_HOST]}
|
||||
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aiocomelit"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["aiocomelit==0.12.0"]
|
||||
"requirements": ["aiocomelit==0.12.3"]
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
"cannot_authenticate": {
|
||||
"message": "Error authenticating"
|
||||
},
|
||||
"updated_failed": {
|
||||
"update_failed": {
|
||||
"message": "Failed to update data: {error}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.4.30"]
|
||||
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.5.7"]
|
||||
}
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["devolo_home_control_api"],
|
||||
"requirements": ["devolo-home-control-api==0.18.3"],
|
||||
"requirements": ["devolo-home-control-api==0.19.0"],
|
||||
"zeroconf": ["_dvl-deviceapi._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"quality_scale": "internal",
|
||||
"requirements": [
|
||||
"aiodhcpwatcher==1.1.1",
|
||||
"aiodiscover==2.6.1",
|
||||
"aiodiscover==2.7.0",
|
||||
"cached-ipaddress==0.10.0"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ async def async_validate_hostname(
|
||||
result = False
|
||||
with contextlib.suppress(DNSError):
|
||||
result = bool(
|
||||
await aiodns.DNSResolver(
|
||||
await aiodns.DNSResolver( # type: ignore[call-overload]
|
||||
nameservers=[resolver], udp_port=port, tcp_port=port
|
||||
).query(hostname, qtype)
|
||||
)
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/dnsip",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["aiodns==3.2.0"]
|
||||
"requirements": ["aiodns==3.4.0"]
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ class WanIpSensor(SensorEntity):
|
||||
async def async_update(self) -> None:
|
||||
"""Get the current DNS IP address for hostname."""
|
||||
try:
|
||||
response = await self.resolver.query(self.hostname, self.querytype)
|
||||
response = await self.resolver.query(self.hostname, self.querytype) # type: ignore[call-overload]
|
||||
except DNSError as err:
|
||||
_LOGGER.warning("Exception while resolving host: %s", err)
|
||||
response = None
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||
"requirements": ["py-sucks==0.9.10", "deebot-client==13.0.1"]
|
||||
"requirements": ["py-sucks==0.9.10", "deebot-client==13.2.1"]
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@ from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Generic
|
||||
|
||||
from deebot_client.capabilities import CapabilityEvent, CapabilityLifeSpan
|
||||
from deebot_client.capabilities import CapabilityEvent, CapabilityLifeSpan, DeviceType
|
||||
from deebot_client.device import Device
|
||||
from deebot_client.events import (
|
||||
BatteryEvent,
|
||||
ErrorEvent,
|
||||
@@ -34,7 +35,7 @@ from homeassistant.const import (
|
||||
UnitOfArea,
|
||||
UnitOfTime,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
@@ -59,6 +60,15 @@ class EcovacsSensorEntityDescription(
|
||||
"""Ecovacs sensor entity description."""
|
||||
|
||||
value_fn: Callable[[EventT], StateType]
|
||||
native_unit_of_measurement_fn: Callable[[DeviceType], str | None] | None = None
|
||||
|
||||
|
||||
@callback
|
||||
def get_area_native_unit_of_measurement(device_type: DeviceType) -> str | None:
|
||||
"""Get the area native unit of measurement based on device type."""
|
||||
if device_type is DeviceType.MOWER:
|
||||
return UnitOfArea.SQUARE_CENTIMETERS
|
||||
return UnitOfArea.SQUARE_METERS
|
||||
|
||||
|
||||
ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = (
|
||||
@@ -68,7 +78,9 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = (
|
||||
capability_fn=lambda caps: caps.stats.clean,
|
||||
value_fn=lambda e: e.area,
|
||||
translation_key="stats_area",
|
||||
native_unit_of_measurement=UnitOfArea.SQUARE_METERS,
|
||||
device_class=SensorDeviceClass.AREA,
|
||||
native_unit_of_measurement_fn=get_area_native_unit_of_measurement,
|
||||
suggested_unit_of_measurement=UnitOfArea.SQUARE_METERS,
|
||||
),
|
||||
EcovacsSensorEntityDescription[StatsEvent](
|
||||
key="stats_time",
|
||||
@@ -85,6 +97,7 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = (
|
||||
value_fn=lambda e: e.area,
|
||||
key="total_stats_area",
|
||||
translation_key="total_stats_area",
|
||||
device_class=SensorDeviceClass.AREA,
|
||||
native_unit_of_measurement=UnitOfArea.SQUARE_METERS,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
@@ -249,6 +262,27 @@ class EcovacsSensor(
|
||||
|
||||
entity_description: EcovacsSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device: Device,
|
||||
capability: CapabilityEvent,
|
||||
entity_description: EcovacsSensorEntityDescription,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize entity."""
|
||||
super().__init__(device, capability, entity_description, **kwargs)
|
||||
if (
|
||||
entity_description.native_unit_of_measurement_fn
|
||||
and (
|
||||
native_unit_of_measurement
|
||||
:= entity_description.native_unit_of_measurement_fn(
|
||||
device.capabilities.device_type
|
||||
)
|
||||
)
|
||||
is not None
|
||||
):
|
||||
self._attr_native_unit_of_measurement = native_unit_of_measurement
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Set up the event listeners now that hass is ready."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["sense_energy"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["sense-energy==0.13.7"]
|
||||
"requirements": ["sense-energy==0.13.8"]
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ async def _get_fixture_collection(envoy: Envoy, serial: str) -> dict[str, Any]:
|
||||
"/ivp/ensemble/generator",
|
||||
"/ivp/meters",
|
||||
"/ivp/meters/readings",
|
||||
"/home,",
|
||||
"/home",
|
||||
]
|
||||
|
||||
for end_point in end_points:
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyenphase"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pyenphase==1.26.0"],
|
||||
"requirements": ["pyenphase==1.26.1"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_enphase-envoy._tcp.local."
|
||||
|
||||
@@ -22,5 +22,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["eq3btsmart"],
|
||||
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.14.0"]
|
||||
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.15.1"]
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_IGNORE,
|
||||
SOURCE_REAUTH,
|
||||
SOURCE_RECONFIGURE,
|
||||
ConfigEntry,
|
||||
@@ -31,6 +32,7 @@ from homeassistant.config_entries import (
|
||||
)
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import AbortFlow
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
|
||||
from homeassistant.helpers.service_info.hassio import HassioServiceInfo
|
||||
@@ -302,7 +304,15 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
):
|
||||
return
|
||||
if entry.source == SOURCE_IGNORE:
|
||||
# Don't call _fetch_device_info() for ignored entries
|
||||
raise AbortFlow("already_configured")
|
||||
configured_host: str | None = entry.data.get(CONF_HOST)
|
||||
configured_port: int | None = entry.data.get(CONF_PORT)
|
||||
if configured_host == host and configured_port == port:
|
||||
# Don't probe to verify the mac is correct since
|
||||
# the host and port matches.
|
||||
raise AbortFlow("already_configured")
|
||||
configured_psk: str | None = entry.data.get(CONF_NOISE_PSK)
|
||||
await self._fetch_device_info(host, port or configured_port, configured_psk)
|
||||
updates: dict[str, Any] = {}
|
||||
|
||||
@@ -17,7 +17,7 @@ DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS = False
|
||||
|
||||
DEFAULT_PORT: Final = 6053
|
||||
|
||||
STABLE_BLE_VERSION_STR = "2025.2.2"
|
||||
STABLE_BLE_VERSION_STR = "2025.5.0"
|
||||
STABLE_BLE_VERSION = AwesomeVersion(STABLE_BLE_VERSION_STR)
|
||||
PROJECT_URLS = {
|
||||
"esphome.bluetooth-proxy": "https://esphome.github.io/bluetooth-proxies/",
|
||||
|
||||
@@ -223,7 +223,6 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]):
|
||||
self._states = cast(dict[int, _StateT], entry_data.state[state_type])
|
||||
assert entry_data.device_info is not None
|
||||
device_info = entry_data.device_info
|
||||
self._device_info = device_info
|
||||
self._on_entry_data_changed()
|
||||
self._key = entity_info.key
|
||||
self._state_type = state_type
|
||||
@@ -311,6 +310,11 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]):
|
||||
@callback
|
||||
def _on_entry_data_changed(self) -> None:
|
||||
entry_data = self._entry_data
|
||||
# Update the device info since it can change
|
||||
# when the device is reconnected
|
||||
if TYPE_CHECKING:
|
||||
assert entry_data.device_info is not None
|
||||
self._device_info = entry_data.device_info
|
||||
self._api_version = entry_data.api_version
|
||||
self._client = entry_data.client
|
||||
if self._device_info.has_deep_sleep:
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"requirements": [
|
||||
"aioesphomeapi==30.1.0",
|
||||
"esphome-dashboard-api==1.3.0",
|
||||
"bleak-esphome==2.14.0"
|
||||
"bleak-esphome==2.15.1"
|
||||
],
|
||||
"zeroconf": ["_esphomelib._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ from homeassistant.util.enum import try_parse_enum
|
||||
from .const import DOMAIN
|
||||
from .coordinator import ESPHomeDashboardCoordinator
|
||||
from .dashboard import async_get_dashboard
|
||||
from .domain_data import DomainData
|
||||
from .entity import (
|
||||
EsphomeEntity,
|
||||
convert_api_error_ha_error,
|
||||
@@ -62,7 +61,7 @@ async def async_setup_entry(
|
||||
|
||||
if (dashboard := async_get_dashboard(hass)) is None:
|
||||
return
|
||||
entry_data = DomainData.get(hass).get_entry_data(entry)
|
||||
entry_data = entry.runtime_data
|
||||
assert entry_data.device_info is not None
|
||||
device_name = entry_data.device_info.name
|
||||
unsubs: list[CALLBACK_TYPE] = []
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyfibaro"],
|
||||
"requirements": ["pyfibaro==0.8.2"]
|
||||
"requirements": ["pyfibaro==0.8.3"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/forecast_solar",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["forecast-solar==4.1.0"]
|
||||
"requirements": ["forecast-solar==4.2.0"]
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -92,7 +92,7 @@ class FritzboxDataUpdateCoordinator(DataUpdateCoordinator[FritzboxCoordinatorDat
|
||||
|
||||
available_main_ains = [
|
||||
ain
|
||||
for ain, dev in data.devices.items()
|
||||
for ain, dev in data.devices.items() | data.templates.items()
|
||||
if dev.device_and_unit_id[1] is None
|
||||
]
|
||||
device_reg = dr.async_get(self.hass)
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,15 @@ type FroniusConfigEntry = ConfigEntry[FroniusSolarNet]
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: FroniusConfigEntry) -> bool:
|
||||
"""Set up fronius from a config entry."""
|
||||
host = entry.data[CONF_HOST]
|
||||
fronius = Fronius(async_get_clientsession(hass), host)
|
||||
fronius = Fronius(
|
||||
async_get_clientsession(
|
||||
hass,
|
||||
# Fronius Gen24 firmware 1.35.4-1 redirects to HTTPS with self-signed
|
||||
# certificate. See https://github.com/home-assistant/core/issues/138881
|
||||
verify_ssl=False,
|
||||
),
|
||||
host,
|
||||
)
|
||||
solar_net = FroniusSolarNet(hass, entry, fronius)
|
||||
await solar_net.init_devices()
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ async def validate_host(
|
||||
hass: HomeAssistant, host: str
|
||||
) -> tuple[str, FroniusConfigEntryData]:
|
||||
"""Validate the user input allows us to connect."""
|
||||
fronius = Fronius(async_get_clientsession(hass), host)
|
||||
fronius = Fronius(async_get_clientsession(hass, verify_ssl=False), host)
|
||||
|
||||
try:
|
||||
datalogger_info: dict[str, Any]
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20250430.2"]
|
||||
"requirements": ["home-assistant-frontend==20250516.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.1", "oauth2client==4.1.3", "ical==9.2.4"]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -254,11 +254,11 @@ async def google_generative_ai_config_option_schema(
|
||||
)
|
||||
for api_model in sorted(api_models, key=lambda x: x.display_name or "")
|
||||
if (
|
||||
api_model.name != "models/gemini-1.0-pro" # duplicate of gemini-pro
|
||||
and api_model.display_name
|
||||
api_model.display_name
|
||||
and api_model.name
|
||||
and api_model.supported_actions
|
||||
and "tts" not in api_model.name
|
||||
and "vision" not in api_model.name
|
||||
and api_model.supported_actions
|
||||
and "generateContent" in api_model.supported_actions
|
||||
)
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -319,11 +319,10 @@ class GoogleGenerativeAIConversationEntity(
|
||||
tools.append(Tool(google_search=GoogleSearch()))
|
||||
|
||||
model_name = self.entry.options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL)
|
||||
# Gemini 1.0 doesn't support system_instruction while 1.5 does.
|
||||
# Assume future versions will support it (if not, the request fails with a
|
||||
# clear message at which point we can fix).
|
||||
# Avoid INVALID_ARGUMENT Developer instruction is not enabled for <model>
|
||||
supports_system_instruction = (
|
||||
"gemini-1.0" not in model_name and "gemini-pro" not in model_name
|
||||
"gemma" not in model_name
|
||||
and "gemini-2.0-flash-preview-image-generation" not in model_name
|
||||
)
|
||||
|
||||
prompt_content = cast(
|
||||
|
||||
@@ -41,12 +41,12 @@
|
||||
},
|
||||
"data_description": {
|
||||
"prompt": "Instruct how the LLM should respond. This can be a template.",
|
||||
"enable_google_search_tool": "Only works with \"No control\" in the \"Control Home Assistant\" setting. See docs for a workaround using it with \"Assist\"."
|
||||
"enable_google_search_tool": "Only works if there is nothing selected in the \"Control Home Assistant\" setting. See docs for a workaround using it with \"Assist\"."
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_google_search_option": "Google Search cannot be enabled alongside any Assist capability, this can only be used when Assist is set to \"No control\"."
|
||||
"invalid_google_search_option": "Google Search can only be enabled if nothing is selected in the \"Control Home Assistant\" setting."
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -60,6 +60,9 @@ class HistoryStats:
|
||||
self._start = start
|
||||
self._end = end
|
||||
|
||||
self._pending_events: list[Event[EventStateChangedData]] = []
|
||||
self._query_count = 0
|
||||
|
||||
async def async_update(
|
||||
self, event: Event[EventStateChangedData] | None
|
||||
) -> HistoryStatsState:
|
||||
@@ -85,6 +88,14 @@ class HistoryStats:
|
||||
utc_now = dt_util.utcnow()
|
||||
now_timestamp = floored_timestamp(utc_now)
|
||||
|
||||
# If we end up querying data from the recorder when we get triggered by a new state
|
||||
# change event, it is possible this function could be reentered a second time before
|
||||
# the first recorder query returns. In that case a second recorder query will be done
|
||||
# and we need to hold the new event so that we can append it after the second query.
|
||||
# Otherwise the event will be dropped.
|
||||
if event:
|
||||
self._pending_events.append(event)
|
||||
|
||||
if current_period_start_timestamp > now_timestamp:
|
||||
# History cannot tell the future
|
||||
self._history_current_period = []
|
||||
@@ -113,15 +124,14 @@ class HistoryStats:
|
||||
start_changed = (
|
||||
current_period_start_timestamp != previous_period_start_timestamp
|
||||
)
|
||||
end_changed = current_period_end_timestamp != previous_period_end_timestamp
|
||||
if start_changed:
|
||||
self._prune_history_cache(current_period_start_timestamp)
|
||||
|
||||
new_data = False
|
||||
if event and (new_state := event.data["new_state"]) is not None:
|
||||
if (
|
||||
current_period_start_timestamp
|
||||
<= floored_timestamp(new_state.last_changed)
|
||||
<= current_period_end_timestamp
|
||||
if current_period_start_timestamp <= floored_timestamp(
|
||||
new_state.last_changed
|
||||
):
|
||||
self._history_current_period.append(
|
||||
HistoryState(new_state.state, new_state.last_changed_timestamp)
|
||||
@@ -131,26 +141,31 @@ class HistoryStats:
|
||||
not new_data
|
||||
and current_period_end_timestamp < now_timestamp
|
||||
and not start_changed
|
||||
and not end_changed
|
||||
):
|
||||
# If period has not changed and current time after the period end...
|
||||
# Don't compute anything as the value cannot have changed
|
||||
return self._state
|
||||
else:
|
||||
await self._async_history_from_db(
|
||||
current_period_start_timestamp, current_period_end_timestamp
|
||||
current_period_start_timestamp, now_timestamp
|
||||
)
|
||||
if event and (new_state := event.data["new_state"]) is not None:
|
||||
if (
|
||||
current_period_start_timestamp
|
||||
<= floored_timestamp(new_state.last_changed)
|
||||
<= current_period_end_timestamp
|
||||
):
|
||||
self._history_current_period.append(
|
||||
HistoryState(new_state.state, new_state.last_changed_timestamp)
|
||||
)
|
||||
for pending_event in self._pending_events:
|
||||
if (new_state := pending_event.data["new_state"]) is not None:
|
||||
if current_period_start_timestamp <= floored_timestamp(
|
||||
new_state.last_changed
|
||||
):
|
||||
self._history_current_period.append(
|
||||
HistoryState(
|
||||
new_state.state, new_state.last_changed_timestamp
|
||||
)
|
||||
)
|
||||
|
||||
self._has_recorder_data = True
|
||||
|
||||
if self._query_count == 0:
|
||||
self._pending_events.clear()
|
||||
|
||||
seconds_matched, match_count = self._async_compute_seconds_and_changes(
|
||||
now_timestamp,
|
||||
current_period_start_timestamp,
|
||||
@@ -165,12 +180,16 @@ class HistoryStats:
|
||||
current_period_end_timestamp: float,
|
||||
) -> None:
|
||||
"""Update history data for the current period from the database."""
|
||||
instance = get_instance(self.hass)
|
||||
states = await instance.async_add_executor_job(
|
||||
self._state_changes_during_period,
|
||||
current_period_start_timestamp,
|
||||
current_period_end_timestamp,
|
||||
)
|
||||
self._query_count += 1
|
||||
try:
|
||||
instance = get_instance(self.hass)
|
||||
states = await instance.async_add_executor_job(
|
||||
self._state_changes_during_period,
|
||||
current_period_start_timestamp,
|
||||
current_period_end_timestamp,
|
||||
)
|
||||
finally:
|
||||
self._query_count -= 1
|
||||
self._history_current_period = [
|
||||
HistoryState(state.state, state.last_changed.timestamp())
|
||||
for state in states
|
||||
@@ -208,6 +227,9 @@ class HistoryStats:
|
||||
current_state_matches = history_state.state in self._entity_states
|
||||
state_change_timestamp = history_state.last_changed
|
||||
|
||||
if math.floor(state_change_timestamp) > end_timestamp:
|
||||
break
|
||||
|
||||
if math.floor(state_change_timestamp) > now_timestamp:
|
||||
# Shouldn't count states that are in the future
|
||||
_LOGGER.debug(
|
||||
@@ -215,7 +237,7 @@ class HistoryStats:
|
||||
state_change_timestamp,
|
||||
now_timestamp,
|
||||
)
|
||||
continue
|
||||
break
|
||||
|
||||
if previous_state_matches:
|
||||
elapsed += state_change_timestamp - last_state_change_timestamp
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["holidays==0.70", "babel==2.15.0"]
|
||||
"requirements": ["holidays==0.73", "babel==2.15.0"]
|
||||
}
|
||||
|
||||
@@ -234,7 +234,7 @@
|
||||
"consumer_products_coffee_maker_program_coffee_world_black_eye": "Black eye",
|
||||
"consumer_products_coffee_maker_program_coffee_world_dead_eye": "Dead eye",
|
||||
"consumer_products_coffee_maker_program_beverage_hot_water": "Hot water",
|
||||
"dishcare_dishwasher_program_pre_rinse": "Pre_rinse",
|
||||
"dishcare_dishwasher_program_pre_rinse": "Pre-rinse",
|
||||
"dishcare_dishwasher_program_auto_1": "Auto 1",
|
||||
"dishcare_dishwasher_program_auto_2": "Auto 2",
|
||||
"dishcare_dishwasher_program_auto_3": "Auto 3",
|
||||
@@ -252,7 +252,7 @@
|
||||
"dishcare_dishwasher_program_intensiv_power": "Intensive power",
|
||||
"dishcare_dishwasher_program_magic_daily": "Magic daily",
|
||||
"dishcare_dishwasher_program_super_60": "Super 60ºC",
|
||||
"dishcare_dishwasher_program_kurz_60": "Kurz 60ºC",
|
||||
"dishcare_dishwasher_program_kurz_60": "Speed 60ºC",
|
||||
"dishcare_dishwasher_program_express_sparkle_65": "Express sparkle 65ºC",
|
||||
"dishcare_dishwasher_program_machine_care": "Machine care",
|
||||
"dishcare_dishwasher_program_steam_fresh": "Steam fresh",
|
||||
@@ -1551,31 +1551,39 @@
|
||||
}
|
||||
},
|
||||
"coffee_counter": {
|
||||
"name": "Coffees"
|
||||
"name": "Coffees",
|
||||
"unit_of_measurement": "coffees"
|
||||
},
|
||||
"powder_coffee_counter": {
|
||||
"name": "Powder coffees"
|
||||
"name": "Powder coffees",
|
||||
"unit_of_measurement": "[%key:component::home_connect::entity::sensor::coffee_counter::unit_of_measurement%]"
|
||||
},
|
||||
"hot_water_counter": {
|
||||
"name": "Hot water"
|
||||
},
|
||||
"hot_water_cups_counter": {
|
||||
"name": "Hot water cups"
|
||||
"name": "Hot water cups",
|
||||
"unit_of_measurement": "cups"
|
||||
},
|
||||
"hot_milk_counter": {
|
||||
"name": "Hot milk cups"
|
||||
"name": "Hot milk cups",
|
||||
"unit_of_measurement": "[%key:component::home_connect::entity::sensor::hot_water_cups_counter::unit_of_measurement%]"
|
||||
},
|
||||
"frothy_milk_counter": {
|
||||
"name": "Frothy milk cups"
|
||||
"name": "Frothy milk cups",
|
||||
"unit_of_measurement": "[%key:component::home_connect::entity::sensor::hot_water_cups_counter::unit_of_measurement%]"
|
||||
},
|
||||
"milk_counter": {
|
||||
"name": "Milk cups"
|
||||
"name": "Milk cups",
|
||||
"unit_of_measurement": "[%key:component::home_connect::entity::sensor::hot_water_cups_counter::unit_of_measurement%]"
|
||||
},
|
||||
"coffee_and_milk_counter": {
|
||||
"name": "Coffee and milk cups"
|
||||
"name": "Coffee and milk cups",
|
||||
"unit_of_measurement": "[%key:component::home_connect::entity::sensor::hot_water_cups_counter::unit_of_measurement%]"
|
||||
},
|
||||
"ristretto_espresso_counter": {
|
||||
"name": "Ristretto espresso cups"
|
||||
"name": "Ristretto espresso cups",
|
||||
"unit_of_measurement": "[%key:component::home_connect::entity::sensor::hot_water_cups_counter::unit_of_measurement%]"
|
||||
},
|
||||
"battery_level": {
|
||||
"name": "Battery level"
|
||||
|
||||
@@ -90,16 +90,17 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
minor_version=2,
|
||||
)
|
||||
|
||||
if config_entry.minor_version == 2:
|
||||
# Add a `firmware_version` key
|
||||
if config_entry.minor_version <= 3:
|
||||
# Add a `firmware_version` key if it doesn't exist to handle entries created
|
||||
# with minor version 1.3 where the firmware version was not set.
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry,
|
||||
data={
|
||||
**config_entry.data,
|
||||
FIRMWARE_VERSION: None,
|
||||
FIRMWARE_VERSION: config_entry.data.get(FIRMWARE_VERSION),
|
||||
},
|
||||
version=1,
|
||||
minor_version=3,
|
||||
minor_version=4,
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
|
||||
@@ -62,7 +62,7 @@ class HomeAssistantYellowConfigFlow(BaseFirmwareConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Home Assistant Yellow."""
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 3
|
||||
MINOR_VERSION = 4
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""Instantiate config flow."""
|
||||
@@ -116,6 +116,11 @@ class HomeAssistantYellowConfigFlow(BaseFirmwareConfigFlow, domain=DOMAIN):
|
||||
if self._probed_firmware_info is not None
|
||||
else ApplicationType.EZSP
|
||||
).value,
|
||||
FIRMWARE_VERSION: (
|
||||
self._probed_firmware_info.firmware_version
|
||||
if self._probed_firmware_info is not None
|
||||
else None
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -8,7 +8,13 @@ from pyhap.const import CATEGORY_AIR_PURIFIER
|
||||
from pyhap.service import Service
|
||||
from pyhap.util import callback as pyhap_callback
|
||||
|
||||
from homeassistant.const import STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN
|
||||
from homeassistant.const import (
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
STATE_ON,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import (
|
||||
Event,
|
||||
EventStateChangedData,
|
||||
@@ -43,7 +49,12 @@ from .const import (
|
||||
THRESHOLD_FILTER_CHANGE_NEEDED,
|
||||
)
|
||||
from .type_fans import ATTR_PRESET_MODE, CHAR_ROTATION_SPEED, Fan
|
||||
from .util import cleanup_name_for_homekit, convert_to_float, density_to_air_quality
|
||||
from .util import (
|
||||
cleanup_name_for_homekit,
|
||||
convert_to_float,
|
||||
density_to_air_quality,
|
||||
temperature_to_homekit,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -345,8 +356,13 @@ class AirPurifier(Fan):
|
||||
):
|
||||
return
|
||||
|
||||
unit = new_state.attributes.get(
|
||||
ATTR_UNIT_OF_MEASUREMENT, UnitOfTemperature.CELSIUS
|
||||
)
|
||||
current_temperature = temperature_to_homekit(current_temperature, unit)
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s: Linked temperature sensor %s changed to %d",
|
||||
"%s: Linked temperature sensor %s changed to %d °C",
|
||||
self.entity_id,
|
||||
self.linked_temperature_sensor,
|
||||
current_temperature,
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
"""Support for HomematicIP Cloud events."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from homematicip.base.channel_event import ChannelEvent
|
||||
from homematicip.base.functionalChannels import FunctionalChannel
|
||||
from homematicip.device import Device
|
||||
|
||||
from homeassistant.components.event import (
|
||||
@@ -23,6 +26,9 @@ from .hap import HomematicipHAP
|
||||
class HmipEventEntityDescription(EventEntityDescription):
|
||||
"""Description of a HomematicIP Cloud event."""
|
||||
|
||||
channel_event_types: list[str] | None = None
|
||||
channel_selector_fn: Callable[[FunctionalChannel], bool] | None = None
|
||||
|
||||
|
||||
EVENT_DESCRIPTIONS = {
|
||||
"doorbell": HmipEventEntityDescription(
|
||||
@@ -30,6 +36,8 @@ EVENT_DESCRIPTIONS = {
|
||||
translation_key="doorbell",
|
||||
device_class=EventDeviceClass.DOORBELL,
|
||||
event_types=["ring"],
|
||||
channel_event_types=["DOOR_BELL_SENSOR_EVENT"],
|
||||
channel_selector_fn=lambda channel: channel.channelRole == "DOOR_BELL_INPUT",
|
||||
),
|
||||
}
|
||||
|
||||
@@ -41,24 +49,29 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the HomematicIP cover from a config entry."""
|
||||
hap = hass.data[DOMAIN][config_entry.unique_id]
|
||||
entities: list[HomematicipGenericEntity] = []
|
||||
|
||||
async_add_entities(
|
||||
entities.extend(
|
||||
HomematicipDoorBellEvent(
|
||||
hap,
|
||||
device,
|
||||
channel.index,
|
||||
EVENT_DESCRIPTIONS["doorbell"],
|
||||
description,
|
||||
)
|
||||
for description in EVENT_DESCRIPTIONS.values()
|
||||
for device in hap.home.devices
|
||||
for channel in device.functionalChannels
|
||||
if channel.channelRole == "DOOR_BELL_INPUT"
|
||||
if description.channel_selector_fn and description.channel_selector_fn(channel)
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class HomematicipDoorBellEvent(HomematicipGenericEntity, EventEntity):
|
||||
"""Event class for HomematicIP doorbell events."""
|
||||
|
||||
_attr_device_class = EventDeviceClass.DOORBELL
|
||||
entity_description: HmipEventEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -86,9 +99,27 @@ class HomematicipDoorBellEvent(HomematicipGenericEntity, EventEntity):
|
||||
@callback
|
||||
def _async_handle_event(self, *args, **kwargs) -> None:
|
||||
"""Handle the event fired by the functional channel."""
|
||||
raised_channel_event = self._get_channel_event_from_args(*args)
|
||||
|
||||
if not self._should_raise(raised_channel_event):
|
||||
return
|
||||
|
||||
event_types = self.entity_description.event_types
|
||||
if TYPE_CHECKING:
|
||||
assert event_types is not None
|
||||
|
||||
self._trigger_event(event_type=event_types[0])
|
||||
self.async_write_ha_state()
|
||||
|
||||
def _should_raise(self, event_type: str) -> bool:
|
||||
"""Check if the event should be raised."""
|
||||
if self.entity_description.channel_event_types is None:
|
||||
return False
|
||||
return event_type in self.entity_description.channel_event_types
|
||||
|
||||
def _get_channel_event_from_args(self, *args) -> str:
|
||||
"""Get the channel event."""
|
||||
if isinstance(args[0], ChannelEvent):
|
||||
return args[0].channelEventType
|
||||
|
||||
return ""
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -110,14 +110,14 @@ class AutomowerLawnMowerEntity(AutomowerAvailableEntity, LawnMowerEntity):
|
||||
mower_attributes = self.mower_attributes
|
||||
if mower_attributes.mower.state in PAUSED_STATES:
|
||||
return LawnMowerActivity.PAUSED
|
||||
if mower_attributes.mower.activity in MOWING_ACTIVITIES:
|
||||
return LawnMowerActivity.MOWING
|
||||
if mower_attributes.mower.activity == MowerActivities.GOING_HOME:
|
||||
return LawnMowerActivity.RETURNING
|
||||
if (mower_attributes.mower.state == "RESTRICTED") or (
|
||||
mower_attributes.mower.activity in DOCKED_ACTIVITIES
|
||||
):
|
||||
return LawnMowerActivity.DOCKED
|
||||
if mower_attributes.mower.state in MowerStates.IN_OPERATION:
|
||||
if mower_attributes.mower.activity == MowerActivities.GOING_HOME:
|
||||
return LawnMowerActivity.RETURNING
|
||||
return LawnMowerActivity.MOWING
|
||||
return LawnMowerActivity.ERROR
|
||||
|
||||
@property
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aioautomower"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aioautomower==2025.4.4"]
|
||||
"requirements": ["aioautomower==2025.5.1"]
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ class INKBIRDActiveBluetoothProcessorCoordinator(
|
||||
update_method=self._async_on_update,
|
||||
needs_poll_method=self._async_needs_poll,
|
||||
poll_method=self._async_poll_data,
|
||||
connectable=False, # Polling only happens if active scanning is disabled
|
||||
)
|
||||
|
||||
async def async_init(self) -> None:
|
||||
|
||||
@@ -34,6 +34,10 @@
|
||||
"local_name": "ITH-21-B",
|
||||
"connectable": false
|
||||
},
|
||||
{
|
||||
"local_name": "IBS-P02B",
|
||||
"connectable": false
|
||||
},
|
||||
{
|
||||
"local_name": "Ink@IAM-T1",
|
||||
"connectable": true
|
||||
@@ -49,5 +53,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/inkbird",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["inkbird-ble==0.15.0"]
|
||||
"requirements": ["inkbird-ble==0.16.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
|
||||
|
||||
@@ -401,8 +401,7 @@ def _categorize_programs(isy_data: IsyData, programs: Programs) -> None:
|
||||
for dtype, _, node_id in folder.children:
|
||||
if dtype != TAG_FOLDER:
|
||||
continue
|
||||
entity_folder = folder[node_id]
|
||||
|
||||
entity_folder: Programs = folder[node_id]
|
||||
actions = None
|
||||
status = entity_folder.get_by_name(KEY_STATUS)
|
||||
if not status or status.protocol != PROTO_PROGRAM:
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyisy"],
|
||||
"requirements": ["pyisy==3.4.0"],
|
||||
"requirements": ["pyisy==3.4.1"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Universal Devices Inc.",
|
||||
|
||||
@@ -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],
|
||||
@@ -70,7 +70,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) -
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN, translation_key="authentication_failed"
|
||||
) from ex
|
||||
except RequestNotSuccessful as ex:
|
||||
except (RequestNotSuccessful, TimeoutError) as ex:
|
||||
_LOGGER.debug(ex, exc_info=True)
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN, translation_key="api_error"
|
||||
|
||||
@@ -5,7 +5,7 @@ from dataclasses import dataclass
|
||||
from typing import cast
|
||||
|
||||
from pylamarzocco import LaMarzoccoMachine
|
||||
from pylamarzocco.const import BackFlushStatus, MachineState, WidgetType
|
||||
from pylamarzocco.const import BackFlushStatus, MachineState, ModelName, WidgetType
|
||||
from pylamarzocco.models import BackFlush, MachineStatus
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
@@ -52,7 +52,7 @@ ENTITIES: tuple[LaMarzoccoBinarySensorEntityDescription, ...] = (
|
||||
).status
|
||||
is MachineState.BREWING
|
||||
),
|
||||
available_fn=lambda device: device.websocket.connected,
|
||||
available_fn=lambda coordinator: not coordinator.websocket_terminated,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
LaMarzoccoBinarySensorEntityDescription(
|
||||
@@ -66,6 +66,9 @@ ENTITIES: tuple[LaMarzoccoBinarySensorEntityDescription, ...] = (
|
||||
is BackFlushStatus.REQUESTED
|
||||
),
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
supported_fn=lambda coordinator: (
|
||||
coordinator.device.dashboard.model_name != ModelName.GS3_MP
|
||||
),
|
||||
),
|
||||
LaMarzoccoBinarySensorEntityDescription(
|
||||
key="websocket_connected",
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -44,6 +44,7 @@ class LaMarzoccoUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
|
||||
_default_update_interval = SCAN_INTERVAL
|
||||
config_entry: LaMarzoccoConfigEntry
|
||||
websocket_terminated = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -92,25 +93,37 @@ class LaMarzoccoConfigUpdateCoordinator(LaMarzoccoUpdateCoordinator):
|
||||
await self.device.get_dashboard()
|
||||
_LOGGER.debug("Current status: %s", self.device.dashboard.to_dict())
|
||||
|
||||
_LOGGER.debug("Init WebSocket in background task")
|
||||
|
||||
self.config_entry.async_create_background_task(
|
||||
hass=self.hass,
|
||||
target=self.device.connect_dashboard_websocket(
|
||||
update_callback=lambda _: self.async_set_updated_data(None)
|
||||
),
|
||||
target=self.connect_websocket(),
|
||||
name="lm_websocket_task",
|
||||
)
|
||||
|
||||
async def websocket_close(_: Any | None = None) -> None:
|
||||
if self.device.websocket.connected:
|
||||
await self.device.websocket.disconnect()
|
||||
await self.device.websocket.disconnect()
|
||||
|
||||
self.config_entry.async_on_unload(
|
||||
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, websocket_close)
|
||||
)
|
||||
self.config_entry.async_on_unload(websocket_close)
|
||||
|
||||
async def connect_websocket(self) -> None:
|
||||
"""Connect to the websocket."""
|
||||
|
||||
_LOGGER.debug("Init WebSocket in background task")
|
||||
|
||||
self.websocket_terminated = False
|
||||
self.async_update_listeners()
|
||||
|
||||
await self.device.connect_dashboard_websocket(
|
||||
update_callback=lambda _: self.async_set_updated_data(None),
|
||||
connect_callback=self.async_update_listeners,
|
||||
disconnect_callback=self.async_update_listeners,
|
||||
)
|
||||
|
||||
self.websocket_terminated = True
|
||||
self.async_update_listeners()
|
||||
|
||||
|
||||
class LaMarzoccoSettingsUpdateCoordinator(LaMarzoccoUpdateCoordinator):
|
||||
"""Coordinator for La Marzocco settings."""
|
||||
|
||||
@@ -5,8 +5,10 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.const import CONF_MAC, CONF_TOKEN
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import CONF_USE_BLUETOOTH
|
||||
from .coordinator import LaMarzoccoConfigEntry
|
||||
|
||||
TO_REDACT = {
|
||||
@@ -21,4 +23,12 @@ async def async_get_config_entry_diagnostics(
|
||||
"""Return diagnostics for a config entry."""
|
||||
coordinator = entry.runtime_data.config_coordinator
|
||||
device = coordinator.device
|
||||
return async_redact_data(device.to_dict(), TO_REDACT)
|
||||
data = {
|
||||
"device": device.to_dict(),
|
||||
"bluetooth_available": {
|
||||
"options_enabled": entry.options.get(CONF_USE_BLUETOOTH, True),
|
||||
CONF_MAC: CONF_MAC in entry.data,
|
||||
CONF_TOKEN: CONF_TOKEN in entry.data,
|
||||
},
|
||||
}
|
||||
return async_redact_data(data, TO_REDACT)
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from pylamarzocco import LaMarzoccoMachine
|
||||
from pylamarzocco.const import FirmwareType
|
||||
|
||||
from homeassistant.const import CONF_ADDRESS, CONF_MAC
|
||||
@@ -23,7 +22,7 @@ from .coordinator import LaMarzoccoUpdateCoordinator
|
||||
class LaMarzoccoEntityDescription(EntityDescription):
|
||||
"""Description for all LM entities."""
|
||||
|
||||
available_fn: Callable[[LaMarzoccoMachine], bool] = lambda _: True
|
||||
available_fn: Callable[[LaMarzoccoUpdateCoordinator], bool] = lambda _: True
|
||||
supported_fn: Callable[[LaMarzoccoUpdateCoordinator], bool] = lambda _: True
|
||||
|
||||
|
||||
@@ -74,7 +73,7 @@ class LaMarzoccoEntity(LaMarzoccoBaseEntity):
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
if super().available:
|
||||
return self.entity_description.available_fn(self.coordinator.device)
|
||||
return self.entity_description.available_fn(self.coordinator)
|
||||
return False
|
||||
|
||||
def __init__(
|
||||
|
||||
@@ -37,5 +37,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pylamarzocco"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pylamarzocco==2.0.0b6"]
|
||||
"requirements": ["pylamarzocco==2.0.4"]
|
||||
}
|
||||
|
||||
@@ -100,8 +100,9 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
|
||||
.seconds.seconds_out
|
||||
),
|
||||
available_fn=(
|
||||
lambda machine: cast(
|
||||
PreBrewing, machine.dashboard.config[WidgetType.CM_PRE_BREWING]
|
||||
lambda coordinator: cast(
|
||||
PreBrewing,
|
||||
coordinator.device.dashboard.config[WidgetType.CM_PRE_BREWING],
|
||||
).mode
|
||||
is PreExtractionMode.PREINFUSION
|
||||
),
|
||||
@@ -140,8 +141,8 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
|
||||
.times.pre_brewing[0]
|
||||
.seconds.seconds_in
|
||||
),
|
||||
available_fn=lambda machine: cast(
|
||||
PreBrewing, machine.dashboard.config[WidgetType.CM_PRE_BREWING]
|
||||
available_fn=lambda coordinator: cast(
|
||||
PreBrewing, coordinator.device.dashboard.config[WidgetType.CM_PRE_BREWING]
|
||||
).mode
|
||||
is PreExtractionMode.PREBREWING,
|
||||
supported_fn=(
|
||||
@@ -180,8 +181,9 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
|
||||
.seconds.seconds_out
|
||||
),
|
||||
available_fn=(
|
||||
lambda machine: cast(
|
||||
PreBrewing, machine.dashboard.config[WidgetType.CM_PRE_BREWING]
|
||||
lambda coordinator: cast(
|
||||
PreBrewing,
|
||||
coordinator.device.dashboard.config[WidgetType.CM_PRE_BREWING],
|
||||
).mode
|
||||
is PreExtractionMode.PREBREWING
|
||||
),
|
||||
|
||||
@@ -132,17 +132,18 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up sensor entities."""
|
||||
coordinator = entry.runtime_data.config_coordinator
|
||||
config_coordinator = entry.runtime_data.config_coordinator
|
||||
statistic_coordinators = entry.runtime_data.statistics_coordinator
|
||||
|
||||
entities = [
|
||||
LaMarzoccoSensorEntity(coordinator, description)
|
||||
LaMarzoccoSensorEntity(config_coordinator, description)
|
||||
for description in ENTITIES
|
||||
if description.supported_fn(coordinator)
|
||||
if description.supported_fn(config_coordinator)
|
||||
]
|
||||
entities.extend(
|
||||
LaMarzoccoStatisticSensorEntity(coordinator, description)
|
||||
LaMarzoccoStatisticSensorEntity(statistic_coordinators, description)
|
||||
for description in STATISTIC_ENTITIES
|
||||
if description.supported_fn(coordinator)
|
||||
if description.supported_fn(statistic_coordinators)
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import asyncio
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from pylamarzocco.const import FirmwareType, UpdateCommandStatus
|
||||
from pylamarzocco.const import FirmwareType, UpdateStatus
|
||||
from pylamarzocco.exceptions import RequestNotSuccessful
|
||||
|
||||
from homeassistant.components.update import (
|
||||
@@ -125,7 +125,7 @@ class LaMarzoccoUpdateEntity(LaMarzoccoEntity, UpdateEntity):
|
||||
await self.coordinator.device.update_firmware()
|
||||
while (
|
||||
update_progress := await self.coordinator.device.get_firmware()
|
||||
).command_status is UpdateCommandStatus.IN_PROGRESS:
|
||||
).command_status is UpdateStatus.IN_PROGRESS:
|
||||
if counter >= MAX_UPDATE_WAIT:
|
||||
_raise_timeout_error()
|
||||
self._attr_update_percentage = update_progress.progress_percentage
|
||||
|
||||
@@ -442,7 +442,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
||||
brightness += params.pop(ATTR_BRIGHTNESS_STEP)
|
||||
|
||||
else:
|
||||
brightness += round(params.pop(ATTR_BRIGHTNESS_STEP_PCT) / 100 * 255)
|
||||
brightness_pct = round(brightness / 255 * 100)
|
||||
brightness = round(
|
||||
(brightness_pct + params.pop(ATTR_BRIGHTNESS_STEP_PCT)) / 100 * 255
|
||||
)
|
||||
|
||||
params[ATTR_BRIGHTNESS] = max(0, min(255, brightness))
|
||||
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["linkplay"],
|
||||
"requirements": ["python-linkplay==0.2.4"],
|
||||
"requirements": ["python-linkplay==0.2.5"],
|
||||
"zeroconf": ["_linkplay._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -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.4"]
|
||||
}
|
||||
|
||||
@@ -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.4"]
|
||||
}
|
||||
|
||||
@@ -9,7 +9,11 @@
|
||||
"condition_type": {
|
||||
"is_locked": "{entity_name} is locked",
|
||||
"is_unlocked": "{entity_name} is unlocked",
|
||||
"is_open": "{entity_name} is open"
|
||||
"is_open": "{entity_name} is open",
|
||||
"is_jammed": "{entity_name} is jammed",
|
||||
"is_locking": "{entity_name} is locking",
|
||||
"is_unlocking": "{entity_name} is unlocking",
|
||||
"is_opening": "{entity_name} is opening"
|
||||
},
|
||||
"trigger_type": {
|
||||
"locked": "{entity_name} locked",
|
||||
|
||||
@@ -475,7 +475,7 @@ class MatrixBot:
|
||||
file_stat = await aiofiles.os.stat(image_path)
|
||||
|
||||
_LOGGER.debug("Uploading file from path, %s", image_path)
|
||||
async with aiofiles.open(image_path, "r+b") as image_file:
|
||||
async with aiofiles.open(image_path, "rb") as image_file:
|
||||
response, _ = await self._client.upload(
|
||||
image_file,
|
||||
content_type=mime_type,
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"iot_class": "calculated",
|
||||
"loggers": ["yt_dlp"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["yt-dlp[default]==2025.03.31"],
|
||||
"requirements": ["yt-dlp[default]==2025.05.22"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -291,7 +291,7 @@
|
||||
"description": "The term to search for."
|
||||
},
|
||||
"media_filter_classes": {
|
||||
"name": "Media filter classes",
|
||||
"name": "Media class filter",
|
||||
"description": "List of media classes to filter the search results by."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,8 +57,8 @@ ATA_HVAC_MODE_REVERSE_LOOKUP = {v: k for k, v in ATA_HVAC_MODE_LOOKUP.items()}
|
||||
|
||||
|
||||
ATW_ZONE_HVAC_MODE_LOOKUP = {
|
||||
atw.ZONE_OPERATION_MODE_HEAT: HVACMode.HEAT,
|
||||
atw.ZONE_OPERATION_MODE_COOL: HVACMode.COOL,
|
||||
atw.ZONE_STATUS_HEAT: HVACMode.HEAT,
|
||||
atw.ZONE_STATUS_COOL: HVACMode.COOL,
|
||||
}
|
||||
ATW_ZONE_HVAC_MODE_REVERSE_LOOKUP = {v: k for k, v in ATW_ZONE_HVAC_MODE_LOOKUP.items()}
|
||||
|
||||
|
||||
@@ -131,8 +131,7 @@ class MieleButton(MieleEntity, ButtonEntity):
|
||||
|
||||
return (
|
||||
super().available
|
||||
and self.entity_description.press_data
|
||||
in self.coordinator.data.actions[self._device_id].process_actions
|
||||
and self.entity_description.press_data in self.action.process_actions
|
||||
)
|
||||
|
||||
async def async_press(self) -> None:
|
||||
|
||||
@@ -174,6 +174,11 @@ class MieleClimate(MieleEntity, ClimateEntity):
|
||||
t_key = ZONE1_DEVICES.get(
|
||||
cast(MieleAppliance, self.device.device_type), "zone_1"
|
||||
)
|
||||
if self.device.device_type in (
|
||||
MieleAppliance.FRIDGE,
|
||||
MieleAppliance.FREEZER,
|
||||
):
|
||||
self._attr_name = None
|
||||
|
||||
if description.zone == 2:
|
||||
if self.device.device_type in (
|
||||
@@ -192,8 +197,7 @@ class MieleClimate(MieleEntity, ClimateEntity):
|
||||
@property
|
||||
def target_temperature(self) -> float | None:
|
||||
"""Return the target temperature."""
|
||||
if self.entity_description.target_fn(self.device) is None:
|
||||
return None
|
||||
|
||||
return cast(float | None, self.entity_description.target_fn(self.device))
|
||||
|
||||
@property
|
||||
@@ -201,9 +205,7 @@ class MieleClimate(MieleEntity, ClimateEntity):
|
||||
"""Return the maximum target temperature."""
|
||||
return cast(
|
||||
float,
|
||||
self.coordinator.data.actions[self._device_id]
|
||||
.target_temperature[self.entity_description.zone - 1]
|
||||
.max,
|
||||
self.action.target_temperature[self.entity_description.zone - 1].max,
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -211,9 +213,7 @@ class MieleClimate(MieleEntity, ClimateEntity):
|
||||
"""Return the minimum target temperature."""
|
||||
return cast(
|
||||
float,
|
||||
self.coordinator.data.actions[self._device_id]
|
||||
.target_temperature[self.entity_description.zone - 1]
|
||||
.min,
|
||||
self.action.target_temperature[self.entity_description.zone - 1].min,
|
||||
)
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user