forked from home-assistant/core
Compare commits
184 Commits
whirlpool_
...
2025.5.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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%]",
|
||||
|
||||
@@ -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%]"
|
||||
@@ -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:
|
||||
|
||||
@@ -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.1"]
|
||||
}
|
||||
|
||||
@@ -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.0"]
|
||||
}
|
||||
|
||||
@@ -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,7 @@ 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,
|
||||
native_unit_of_measurement_fn=get_area_native_unit_of_measurement,
|
||||
),
|
||||
EcovacsSensorEntityDescription[StatsEvent](
|
||||
key="stats_time",
|
||||
@@ -85,7 +95,7 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = (
|
||||
value_fn=lambda e: e.area,
|
||||
key="total_stats_area",
|
||||
translation_key="total_stats_area",
|
||||
native_unit_of_measurement=UnitOfArea.SQUARE_METERS,
|
||||
native_unit_of_measurement_fn=get_area_native_unit_of_measurement,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
EcovacsSensorEntityDescription[TotalStatsEvent](
|
||||
@@ -249,6 +259,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()
|
||||
|
||||
@@ -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] = {}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -26,3 +26,4 @@ CONF_USE_GOOGLE_SEARCH_TOOL = "enable_google_search_tool"
|
||||
RECOMMENDED_USE_GOOGLE_SEARCH_TOOL = False
|
||||
|
||||
TIMEOUT_MILLIS = 10000
|
||||
FILE_POLLING_INTERVAL_SECONDS = 0.05
|
||||
|
||||
@@ -109,6 +109,7 @@ class HassIOIngress(HomeAssistantView):
|
||||
delete = _handle
|
||||
patch = _handle
|
||||
options = _handle
|
||||
head = _handle
|
||||
|
||||
async def _handle_websocket(
|
||||
self, request: web.Request, token: str, path: str
|
||||
|
||||
@@ -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.72", "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"
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.3"]
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -47,7 +47,7 @@ class MieleEntity(CoordinatorEntity[MieleDataUpdateCoordinator]):
|
||||
return self.coordinator.data.devices[self._device_id]
|
||||
|
||||
@property
|
||||
def actions(self) -> MieleAction:
|
||||
def action(self) -> MieleAction:
|
||||
"""Return the actions object."""
|
||||
return self.coordinator.data.actions[self._device_id]
|
||||
|
||||
|
||||
@@ -99,8 +99,10 @@ class MieleFan(MieleEntity, FanEntity):
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return current on/off state."""
|
||||
assert self.device.state_ventilation_step is not None
|
||||
return self.device.state_ventilation_step > 0
|
||||
return (
|
||||
self.device.state_ventilation_step is not None
|
||||
and self.device.state_ventilation_step > 0
|
||||
)
|
||||
|
||||
@property
|
||||
def speed_count(self) -> int:
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pymiele"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pymiele==0.4.1"],
|
||||
"requirements": ["pymiele==0.4.3"],
|
||||
"single_config_entry": true,
|
||||
"zeroconf": ["_mieleathome._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -144,7 +144,6 @@ SENSOR_TYPES: Final[tuple[MieleSensorDefinition, ...]] = (
|
||||
MieleAppliance.STEAM_OVEN,
|
||||
MieleAppliance.MICROWAVE,
|
||||
MieleAppliance.COFFEE_SYSTEM,
|
||||
MieleAppliance.ROBOT_VACUUM_CLEANER,
|
||||
MieleAppliance.WASHER_DRYER,
|
||||
MieleAppliance.STEAM_OVEN_COMBI,
|
||||
MieleAppliance.STEAM_OVEN_MICRO,
|
||||
|
||||
@@ -115,9 +115,6 @@
|
||||
},
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"door": {
|
||||
"name": "Door"
|
||||
},
|
||||
"failure": {
|
||||
"name": "Failure"
|
||||
},
|
||||
|
||||
@@ -169,15 +169,14 @@ class MielePowerSwitch(MieleSwitch):
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return the state of the switch."""
|
||||
return self.coordinator.data.actions[self._device_id].power_off_enabled
|
||||
return self.action.power_off_enabled
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return the availability of the entity."""
|
||||
|
||||
return (
|
||||
self.coordinator.data.actions[self._device_id].power_off_enabled
|
||||
or self.coordinator.data.actions[self._device_id].power_on_enabled
|
||||
self.action.power_off_enabled or self.action.power_on_enabled
|
||||
) and super().available
|
||||
|
||||
async def async_turn_switch(self, mode: dict[str, str | int | bool]) -> None:
|
||||
@@ -192,12 +191,8 @@ class MielePowerSwitch(MieleSwitch):
|
||||
"entity": self.entity_id,
|
||||
},
|
||||
) from err
|
||||
self.coordinator.data.actions[self._device_id].power_on_enabled = cast(
|
||||
bool, mode
|
||||
)
|
||||
self.coordinator.data.actions[self._device_id].power_off_enabled = not cast(
|
||||
bool, mode
|
||||
)
|
||||
self.action.power_on_enabled = cast(bool, mode)
|
||||
self.action.power_off_enabled = not cast(bool, mode)
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
TWO_YEARS = 2 * 365 * 24
|
||||
TWO_YEARS_DAYS = 2 * 365
|
||||
|
||||
|
||||
class MillDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
@@ -91,7 +91,7 @@ class MillHistoricDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
if not last_stats or not last_stats.get(statistic_id):
|
||||
hourly_data = (
|
||||
await self.mill_data_connection.fetch_historic_energy_usage(
|
||||
dev_id, n_days=TWO_YEARS
|
||||
dev_id, n_days=TWO_YEARS_DAYS
|
||||
)
|
||||
)
|
||||
hourly_data = dict(sorted(hourly_data.items(), key=lambda x: x[0]))
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/mill",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["mill", "mill_local"],
|
||||
"requirements": ["millheater==0.12.3", "mill-local==0.3.0"]
|
||||
"requirements": ["millheater==0.12.5", "mill-local==0.3.0"]
|
||||
}
|
||||
|
||||
@@ -465,7 +465,7 @@ class PlatformField:
|
||||
required: bool
|
||||
validator: Callable[..., Any]
|
||||
error: str | None = None
|
||||
default: str | int | bool | vol.Undefined = vol.UNDEFINED
|
||||
default: str | int | bool | None | vol.Undefined = vol.UNDEFINED
|
||||
is_schema_default: bool = False
|
||||
exclude_from_reconfig: bool = False
|
||||
conditions: tuple[dict[str, Any], ...] | None = None
|
||||
@@ -498,8 +498,7 @@ def validate_light_platform_config(user_data: dict[str, Any]) -> dict[str, str]:
|
||||
if user_data.get(CONF_MIN_KELVIN, DEFAULT_MIN_KELVIN) >= user_data.get(
|
||||
CONF_MAX_KELVIN, DEFAULT_MAX_KELVIN
|
||||
):
|
||||
errors[CONF_MAX_KELVIN] = "max_below_min_kelvin"
|
||||
errors[CONF_MIN_KELVIN] = "max_below_min_kelvin"
|
||||
errors["advanced_settings"] = "max_below_min_kelvin"
|
||||
return errors
|
||||
|
||||
|
||||
@@ -515,6 +514,7 @@ COMMON_ENTITY_FIELDS = {
|
||||
required=False,
|
||||
validator=str,
|
||||
exclude_from_reconfig=True,
|
||||
default=None,
|
||||
),
|
||||
CONF_ENTITY_PICTURE: PlatformField(
|
||||
selector=TEXT_SELECTOR, required=False, validator=cv.url, error="invalid_url"
|
||||
@@ -1150,7 +1150,7 @@ ENTITY_CONFIG_VALIDATOR: dict[
|
||||
}
|
||||
|
||||
MQTT_DEVICE_PLATFORM_FIELDS = {
|
||||
ATTR_NAME: PlatformField(selector=TEXT_SELECTOR, required=False, validator=str),
|
||||
ATTR_NAME: PlatformField(selector=TEXT_SELECTOR, required=True, validator=str),
|
||||
ATTR_SW_VERSION: PlatformField(
|
||||
selector=TEXT_SELECTOR, required=False, validator=str
|
||||
),
|
||||
@@ -1275,7 +1275,10 @@ def validate_user_input(
|
||||
try:
|
||||
validator(value)
|
||||
except (ValueError, vol.Error, vol.Invalid):
|
||||
errors[field] = data_schema_fields[field].error or "invalid_input"
|
||||
data_schema_field = data_schema_fields[field]
|
||||
errors[data_schema_field.section or field] = (
|
||||
data_schema_field.error or "invalid_input"
|
||||
)
|
||||
|
||||
if config_validator is not None:
|
||||
if TYPE_CHECKING:
|
||||
@@ -1324,7 +1327,10 @@ def data_schema_from_fields(
|
||||
vol.Required(field_name, default=field_details.default)
|
||||
if field_details.required
|
||||
else vol.Optional(
|
||||
field_name, default=field_details.default
|
||||
field_name,
|
||||
default=field_details.default
|
||||
if field_details.default is not None
|
||||
else vol.UNDEFINED,
|
||||
): field_details.selector(component_data_with_user_input) # type: ignore[operator]
|
||||
if field_details.custom_filtering
|
||||
else field_details.selector
|
||||
@@ -1375,12 +1381,17 @@ def data_schema_from_fields(
|
||||
@callback
|
||||
def subentry_schema_default_data_from_fields(
|
||||
data_schema_fields: dict[str, PlatformField],
|
||||
component_data: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
"""Generate custom data schema from platform fields or device data."""
|
||||
return {
|
||||
key: field.default
|
||||
for key, field in data_schema_fields.items()
|
||||
if field.is_schema_default
|
||||
if _check_conditions(field, component_data)
|
||||
and (
|
||||
field.is_schema_default
|
||||
or (field.default is not vol.UNDEFINED and key not in component_data)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -2052,7 +2063,7 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
|
||||
entities = [
|
||||
SelectOptionDict(
|
||||
value=key,
|
||||
label=f"{device_name} {component_data.get(CONF_NAME, '-')}"
|
||||
label=f"{device_name} {component_data.get(CONF_NAME, '-') or '-'}"
|
||||
f" ({component_data[CONF_PLATFORM]})",
|
||||
)
|
||||
for key, component_data in self._subentry_data["components"].items()
|
||||
@@ -2206,7 +2217,10 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
|
||||
for component_data in self._subentry_data["components"].values():
|
||||
platform = component_data[CONF_PLATFORM]
|
||||
subentry_default_data = subentry_schema_default_data_from_fields(
|
||||
PLATFORM_ENTITY_FIELDS[platform]
|
||||
COMMON_ENTITY_FIELDS
|
||||
| PLATFORM_ENTITY_FIELDS[platform]
|
||||
| PLATFORM_MQTT_FIELDS[platform],
|
||||
component_data,
|
||||
)
|
||||
component_data.update(subentry_default_data)
|
||||
|
||||
@@ -2281,7 +2295,8 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
|
||||
self._component_id = None
|
||||
mqtt_device = self._subentry_data[CONF_DEVICE][CONF_NAME]
|
||||
mqtt_items = ", ".join(
|
||||
f"{mqtt_device} {component_data.get(CONF_NAME, '-')} ({component_data[CONF_PLATFORM]})"
|
||||
f"{mqtt_device} {component_data.get(CONF_NAME, '-') or '-'} "
|
||||
f"({component_data[CONF_PLATFORM]})"
|
||||
for component_data in self._subentry_data["components"].values()
|
||||
)
|
||||
menu_options = [
|
||||
|
||||
@@ -256,8 +256,8 @@
|
||||
"green_template": "Green template",
|
||||
"last_reset_value_template": "Last reset value template",
|
||||
"optimistic": "Optimistic",
|
||||
"payload_off": "Payload off",
|
||||
"payload_on": "Payload on",
|
||||
"payload_off": "Payload \"off\"",
|
||||
"payload_on": "Payload \"on\"",
|
||||
"qos": "QoS",
|
||||
"red_template": "Red template",
|
||||
"retain": "Retain",
|
||||
@@ -278,7 +278,7 @@
|
||||
"green_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract green color from the state payload value. Expected result of the template is an integer from 0-255 range.",
|
||||
"last_reset_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the last reset. When Last reset template is set, the State class option must be Total. [Learn more.]({url}#last_reset_value_template)",
|
||||
"force_update": "Sends update events even if the value hasn’t changed. Useful if you want to have meaningful value graphs in history. [Learn more.]({url}#force_update)",
|
||||
"on_command_type": "Defines when the `payload on` is sent. Using `last` (the default) will send any style (brightness, color, etc) topics first and then a `payload on` to the command_topic. Using `first` will send the `payload on` and then any style topics. Using `brightness` will only send brightness commands instead of the `Payload on` to turn the light on.",
|
||||
"on_command_type": "Defines when the payload \"on\" is sent. Using \"Last\" (the default) will send any style (brightness, color, etc) topics first and then a payload \"on\" to the command topic. Using \"First\" will send the payload \"on\" and then any style topics. Using \"Brightness\" will only send brightness commands instead of the payload \"on\" to turn the light on.",
|
||||
"optimistic": "Flag that defines if the {platform} entity works in optimistic mode. [Learn more.]({url}#optimistic)",
|
||||
"payload_off": "The payload that represents the off state.",
|
||||
"payload_on": "The payload that represents the on state.",
|
||||
@@ -287,7 +287,7 @@
|
||||
"retain": "Select if values published by the {platform} entity should be retained at the MQTT broker.",
|
||||
"state_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract state from the state payload value.",
|
||||
"state_topic": "The MQTT topic subscribed to receive {platform} state values. [Learn more.]({url}#state_topic)",
|
||||
"supported_color_modes": "A list of color modes supported by the list. Possible color modes are On/Off, Brightness, Color temperature, HS, XY, RGB, RGBW, RGBWW, WHITE. Note that if onoff or brightness are used, that must be the only value in the list. [Learn more.]({url}#supported_color_modes)",
|
||||
"supported_color_modes": "A list of color modes supported by the list. Possible color modes are On/Off, Brightness, Color temperature, HS, XY, RGB, RGBW, RGBWW, White. Note that if On/Off or Brightness are used, that must be the only value in the list. [Learn more.]({url}#supported_color_modes)",
|
||||
"value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the {platform} entity value. [Learn more.]({url}#value_template)"
|
||||
},
|
||||
"sections": {
|
||||
@@ -325,7 +325,7 @@
|
||||
"data_description": {
|
||||
"brightness": "Flag that defines if light supports brightness when the RGB, RGBW, or RGBWW color mode is supported.",
|
||||
"brightness_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the brightness command topic.",
|
||||
"brightness_command_topic": "The publishing topic that will be used to control the brigthness. [Learn more.]({url}#brightness_command_topic)",
|
||||
"brightness_command_topic": "The publishing topic that will be used to control the brightness. [Learn more.]({url}#brightness_command_topic)",
|
||||
"brightness_scale": "Defines the maximum brightness value (i.e., 100%) of the maximum brightness.",
|
||||
"brightness_state_topic": "The MQTT topic subscribed to receive brightness state values. [Learn more.]({url}#brightness_state_topic)",
|
||||
"brightness_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the brightness value."
|
||||
@@ -385,7 +385,7 @@
|
||||
"hs_value_template": "HS value template"
|
||||
},
|
||||
"data_description": {
|
||||
"hs_command_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose message which will be sent to hs_command_topic. Available variables: `hue` and `sat`.",
|
||||
"hs_command_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose message which will be sent to HS command topic. Available variables: `hue` and `sat`.",
|
||||
"hs_command_topic": "The MQTT topic to publish commands to change the light’s color state in HS format (Hue Saturation). Range for Hue: 0° .. 360°, Range of Saturation: 0..100. Note: Brightness is sent separately in the brightness command topic. [Learn more.]({url}#hs_command_topic)",
|
||||
"hs_state_topic": "The MQTT topic subscribed to receive color state updates in HS format. The expected payload is the hue and saturation values separated by commas, for example, `359.5,100.0`. Note: Brightness is received separately in the brightness state topic. [Learn more.]({url}#hs_state_topic)",
|
||||
"hs_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the HS value."
|
||||
@@ -574,15 +574,15 @@
|
||||
"discovery": "Option to enable MQTT automatic discovery.",
|
||||
"discovery_prefix": "The prefix of configuration topics the MQTT integration will subscribe to.",
|
||||
"birth_enable": "When set, Home Assistant will publish an online message to your MQTT broker when MQTT is ready.",
|
||||
"birth_topic": "The MQTT topic where Home Assistant will publish a `birth` message.",
|
||||
"birth_payload": "The `birth` message that is published when MQTT is ready and connected.",
|
||||
"birth_qos": "The quality of service of the `birth` message that is published when MQTT is ready and connected",
|
||||
"birth_retain": "When set, Home Assistant will retain the `birth` message published to your MQTT broker.",
|
||||
"will_enable": "When set, Home Assistant will ask your broker to publish a `will` message when MQTT is stopped or when it loses the connection to your broker.",
|
||||
"will_topic": "The MQTT topic your MQTT broker will publish a `will` message to.",
|
||||
"will_payload": "The message your MQTT broker `will` publish when the MQTT integration is stopped or when the connection is lost.",
|
||||
"will_qos": "The quality of service of the `will` message that is published by your MQTT broker.",
|
||||
"will_retain": "When set, your MQTT broker will retain the `will` message."
|
||||
"birth_topic": "The MQTT topic where Home Assistant will publish a \"birth\" message.",
|
||||
"birth_payload": "The \"birth\" message that is published when MQTT is ready and connected.",
|
||||
"birth_qos": "The quality of service of the \"birth\" message that is published when MQTT is ready and connected",
|
||||
"birth_retain": "When set, Home Assistant will retain the \"birth\" message published to your MQTT broker.",
|
||||
"will_enable": "When set, Home Assistant will ask your broker to publish a \"will\" message when MQTT is stopped or when it loses the connection to your broker.",
|
||||
"will_topic": "The MQTT topic your MQTT broker will publish a \"will\" message to.",
|
||||
"will_payload": "The message your MQTT broker \"will\" publish when the MQTT integration is stopped or when the connection is lost.",
|
||||
"will_qos": "The quality of service of the \"will\" message that is published by your MQTT broker.",
|
||||
"will_retain": "When set, your MQTT broker will retain the \"will\" message."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -150,7 +150,11 @@ class NetgearRouter:
|
||||
if device_entry.via_device_id is None:
|
||||
continue # do not add the router itself
|
||||
|
||||
device_mac = dict(device_entry.connections)[dr.CONNECTION_NETWORK_MAC]
|
||||
device_mac = dict(device_entry.connections).get(
|
||||
dr.CONNECTION_NETWORK_MAC
|
||||
)
|
||||
if device_mac is None:
|
||||
continue
|
||||
self.devices[device_mac] = {
|
||||
"mac": device_mac,
|
||||
"name": device_entry.name,
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/nextbus",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["py_nextbus"],
|
||||
"requirements": ["py-nextbusnext==2.0.5"]
|
||||
"requirements": ["py-nextbusnext==2.1.2"]
|
||||
}
|
||||
|
||||
@@ -181,11 +181,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
session = aiohttp.ClientSession(connector=connector)
|
||||
|
||||
@callback
|
||||
def _async_close_websession(event: Event) -> None:
|
||||
def _async_close_websession(event: Event | None = None) -> None:
|
||||
"""Close websession."""
|
||||
session.detach()
|
||||
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_close_websession)
|
||||
entry.async_on_unload(_async_close_websession)
|
||||
entry.async_on_unload(
|
||||
hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, _async_close_websession)
|
||||
)
|
||||
|
||||
client = OctoprintClient(
|
||||
host=entry.data[CONF_HOST],
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user