mirror of
https://github.com/home-assistant/core.git
synced 2025-04-20 15:27:52 +00:00
Compare commits
175 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
7af6a4f493 | ||
![]() |
c25f26a290 | ||
![]() |
8d62cb60a6 | ||
![]() |
4f799069ea | ||
![]() |
af708b78e0 | ||
![]() |
f46e659740 | ||
![]() |
7bd517e6ff | ||
![]() |
e9abdab1f5 | ||
![]() |
86eee4f041 | ||
![]() |
9db60c830c | ||
![]() |
c43a4682b9 | ||
![]() |
2a4996055a | ||
![]() |
4643fc2c14 | ||
![]() |
6410b90d82 | ||
![]() |
e5c00eceae | ||
![]() |
fe65579df8 | ||
![]() |
281beecb05 | ||
![]() |
7546b5d269 | ||
![]() |
490e3201b9 | ||
![]() |
04be575139 | ||
![]() |
854cae7f12 | ||
![]() |
109d20978f | ||
![]() |
f8d284ec4b | ||
![]() |
06ebe0810f | ||
![]() |
802ad2ff51 | ||
![]() |
9070a8d579 | ||
![]() |
e8b2a3de8b | ||
![]() |
39549d5dd4 | ||
![]() |
0c19e47bd4 | ||
![]() |
05507d77e3 | ||
![]() |
94558e2d40 | ||
![]() |
4f22fe8f7f | ||
![]() |
9e7dfbb857 | ||
![]() |
02d182239a | ||
![]() |
4e0f581747 | ||
![]() |
42d97d348c | ||
![]() |
69380c85ca | ||
![]() |
b38c647830 | ||
![]() |
2396fd1090 | ||
![]() |
aa4eb89eee | ||
![]() |
1b1bc6af95 | ||
![]() |
f17003a79c | ||
![]() |
ec70e8b0cd | ||
![]() |
d888c70ff0 | ||
![]() |
f29444002e | ||
![]() |
fc66997a36 | ||
![]() |
35513ae072 | ||
![]() |
cd363d48c3 | ||
![]() |
d47ef835d7 | ||
![]() |
00177c699e | ||
![]() |
11b0086a01 | ||
![]() |
ceb177f80e | ||
![]() |
fa3832fbd7 | ||
![]() |
2b9c903429 | ||
![]() |
a7c43f9b49 | ||
![]() |
b428196149 | ||
![]() |
e23da1a90f | ||
![]() |
3951c2ea66 | ||
![]() |
fee152654d | ||
![]() |
51073c948c | ||
![]() |
91438088a0 | ||
![]() |
427e1abdae | ||
![]() |
6e7ac45ac0 | ||
![]() |
4b3b9ebc29 | ||
![]() |
649d8638ed | ||
![]() |
12c4152dbe | ||
![]() |
8f9572bb05 | ||
![]() |
6d022ff4e0 | ||
![]() |
c0c2edb90a | ||
![]() |
b014219fdd | ||
![]() |
216b8ef400 | ||
![]() |
f2ccd46267 | ||
![]() |
e16ba27ce8 | ||
![]() |
506526a6a2 | ||
![]() |
a88678cf42 | ||
![]() |
d0b61af7ec | ||
![]() |
04f5315ab2 | ||
![]() |
7f9e4ba39e | ||
![]() |
06aaf188ea | ||
![]() |
627f994872 | ||
![]() |
9e81ec5aae | ||
![]() |
69753fca1d | ||
![]() |
7773cc121e | ||
![]() |
3aa56936ad | ||
![]() |
e66416c23d | ||
![]() |
a592feae3d | ||
![]() |
fc0d71e891 | ||
![]() |
d4640f1d24 | ||
![]() |
6fe158836e | ||
![]() |
629c0087f4 | ||
![]() |
363bd75129 | ||
![]() |
7592d350a8 | ||
![]() |
8ac8401b4e | ||
![]() |
eed075dbfa | ||
![]() |
23dbdedfb6 | ||
![]() |
85ad29e28e | ||
![]() |
35fc81b038 | ||
![]() |
5d45b84cd2 | ||
![]() |
7766649304 | ||
![]() |
07e9020dfa | ||
![]() |
f504a759e0 | ||
![]() |
9927de4801 | ||
![]() |
1244fc4682 | ||
![]() |
e77a1b12f7 | ||
![]() |
5459daaa10 | ||
![]() |
400131df78 | ||
![]() |
28e1843ff9 | ||
![]() |
df777318d1 | ||
![]() |
6ad5e9e89c | ||
![]() |
a0bd8deee9 | ||
![]() |
405cbd6a00 | ||
![]() |
3e0eb5ab2c | ||
![]() |
fad75a70b6 | ||
![]() |
d9720283df | ||
![]() |
14eed1778b | ||
![]() |
049aaa7e8b | ||
![]() |
35717e8216 | ||
![]() |
2a081abc18 | ||
![]() |
b7f29c7358 | ||
![]() |
3bb6373df5 | ||
![]() |
e1b4edec50 | ||
![]() |
147bee57e1 | ||
![]() |
fcdaea64da | ||
![]() |
d1512d46be | ||
![]() |
0be7db6270 | ||
![]() |
2af0282725 | ||
![]() |
ff458c8417 | ||
![]() |
cc93152ff0 | ||
![]() |
9965f01609 | ||
![]() |
e9c76ce694 | ||
![]() |
58ab7d350d | ||
![]() |
e4d6e20ebd | ||
![]() |
45e273897a | ||
![]() |
d9ec7142d7 | ||
![]() |
e162499267 | ||
![]() |
67f21429e3 | ||
![]() |
a0563f06c9 | ||
![]() |
e7c4fdc8bb | ||
![]() |
c490e350bc | ||
![]() |
e11409ef99 | ||
![]() |
5c8e415a76 | ||
![]() |
e795fb9497 | ||
![]() |
d0afabb85c | ||
![]() |
4f3e8e9b94 | ||
![]() |
46c1cbbc9c | ||
![]() |
8d9a4ea278 | ||
![]() |
22c83e2393 | ||
![]() |
c83a75f6f9 | ||
![]() |
841c727112 | ||
![]() |
d8c9655bfd | ||
![]() |
942ed89cc4 | ||
![]() |
a1fe6b9cf3 | ||
![]() |
2567181cc2 | ||
![]() |
028e4f6029 | ||
![]() |
b82e1a9bef | ||
![]() |
438f226c31 | ||
![]() |
2f139e3cb1 | ||
![]() |
5d75e96fbf | ||
![]() |
dcf2ec5c37 | ||
![]() |
2431e1ba98 | ||
![]() |
4ead108c15 | ||
![]() |
ec8363fa49 | ||
![]() |
e7ff0a3f8b | ||
![]() |
f4c0eb4189 | ||
![]() |
b1ee5a76e1 | ||
![]() |
6b9e8c301b | ||
![]() |
89c3266c7e | ||
![]() |
cff0a632e8 | ||
![]() |
e04d8557ae | ||
![]() |
ca6286f241 | ||
![]() |
35bcc9d5af | ||
![]() |
25b45ce867 | ||
![]() |
d568209bd5 | ||
![]() |
8a43e8af9e | ||
![]() |
785e5b2c16 |
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@ -1480,8 +1480,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/suez_water/ @ooii @jb101010-2
|
||||
/homeassistant/components/sun/ @Swamp-Ig
|
||||
/tests/components/sun/ @Swamp-Ig
|
||||
/homeassistant/components/sunweg/ @rokam
|
||||
/tests/components/sunweg/ @rokam
|
||||
/homeassistant/components/supla/ @mwegrzynek
|
||||
/homeassistant/components/surepetcare/ @benleb @danielhiversen
|
||||
/tests/components/surepetcare/ @benleb @danielhiversen
|
||||
|
@ -859,14 +859,8 @@ async def _async_set_up_integrations(
|
||||
integrations, all_integrations = await _async_resolve_domains_and_preload(
|
||||
hass, config
|
||||
)
|
||||
# Detect all cycles
|
||||
integrations_after_dependencies = (
|
||||
await loader.resolve_integrations_after_dependencies(
|
||||
hass, all_integrations.values(), set(all_integrations)
|
||||
)
|
||||
)
|
||||
all_domains = set(integrations_after_dependencies)
|
||||
domains = set(integrations) & all_domains
|
||||
all_domains = set(all_integrations)
|
||||
domains = set(integrations)
|
||||
|
||||
_LOGGER.info(
|
||||
"Domains to be set up: %s | %s",
|
||||
@ -874,8 +868,6 @@ async def _async_set_up_integrations(
|
||||
all_domains - domains,
|
||||
)
|
||||
|
||||
async_set_domains_to_be_loaded(hass, all_domains)
|
||||
|
||||
# Initialize recorder
|
||||
if "recorder" in all_domains:
|
||||
recorder.async_initialize_recorder(hass)
|
||||
@ -908,12 +900,24 @@ async def _async_set_up_integrations(
|
||||
stage_dep_domains_unfiltered = {
|
||||
dep
|
||||
for domain in stage_domains
|
||||
for dep in integrations_after_dependencies[domain]
|
||||
for dep in all_integrations[domain].all_dependencies
|
||||
if dep not in stage_domains
|
||||
}
|
||||
stage_dep_domains = stage_dep_domains_unfiltered - hass.config.components
|
||||
|
||||
stage_all_domains = stage_domains | stage_dep_domains
|
||||
stage_all_integrations = {
|
||||
domain: all_integrations[domain] for domain in stage_all_domains
|
||||
}
|
||||
# Detect all cycles
|
||||
stage_integrations_after_dependencies = (
|
||||
await loader.resolve_integrations_after_dependencies(
|
||||
hass, stage_all_integrations.values(), stage_all_domains
|
||||
)
|
||||
)
|
||||
stage_all_domains = set(stage_integrations_after_dependencies)
|
||||
stage_domains &= stage_all_domains
|
||||
stage_dep_domains &= stage_all_domains
|
||||
|
||||
_LOGGER.info(
|
||||
"Setting up stage %s: %s | %s\nDependencies: %s | %s",
|
||||
@ -924,6 +928,8 @@ async def _async_set_up_integrations(
|
||||
stage_dep_domains_unfiltered - stage_dep_domains,
|
||||
)
|
||||
|
||||
async_set_domains_to_be_loaded(hass, stage_all_domains)
|
||||
|
||||
if timeout is None:
|
||||
await _async_setup_multi_components(hass, stage_all_domains, config)
|
||||
continue
|
||||
|
5
homeassistant/brands/bosch.json
Normal file
5
homeassistant/brands/bosch.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"domain": "bosch",
|
||||
"name": "Bosch",
|
||||
"integrations": ["bosch_alarm", "bosch_shc", "home_connect"]
|
||||
}
|
5
homeassistant/brands/eve.json
Normal file
5
homeassistant/brands/eve.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"domain": "eve",
|
||||
"name": "Eve",
|
||||
"iot_standards": ["matter"]
|
||||
}
|
@ -8,7 +8,7 @@ from aiohttp import ClientSession
|
||||
from aiohttp.client_exceptions import ClientConnectorError
|
||||
from pyairnow import WebServiceAPI
|
||||
from pyairnow.conv import aqi_to_concentration
|
||||
from pyairnow.errors import AirNowError
|
||||
from pyairnow.errors import AirNowError, InvalidJsonError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
@ -79,7 +79,7 @@ class AirNowDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
distance=self.distance,
|
||||
)
|
||||
|
||||
except (AirNowError, ClientConnectorError) as error:
|
||||
except (AirNowError, ClientConnectorError, InvalidJsonError) as error:
|
||||
raise UpdateFailed(error) from error
|
||||
|
||||
if not obs:
|
||||
|
@ -2,7 +2,7 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"geography_by_coords": {
|
||||
"title": "Configure a Geography",
|
||||
"title": "Configure a geography",
|
||||
"description": "Use the AirVisual cloud API to monitor a latitude/longitude.",
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||
@ -56,12 +56,12 @@
|
||||
"sensor": {
|
||||
"pollutant_label": {
|
||||
"state": {
|
||||
"co": "Carbon Monoxide",
|
||||
"n2": "Nitrogen Dioxide",
|
||||
"co": "Carbon monoxide",
|
||||
"n2": "Nitrogen dioxide",
|
||||
"o3": "Ozone",
|
||||
"p1": "PM10",
|
||||
"p2": "PM2.5",
|
||||
"s2": "Sulfur Dioxide"
|
||||
"s2": "Sulfur dioxide"
|
||||
}
|
||||
},
|
||||
"pollutant_level": {
|
||||
|
@ -20,6 +20,7 @@ import voluptuous as vol
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_IGNORE,
|
||||
SOURCE_REAUTH,
|
||||
SOURCE_ZEROCONF,
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
@ -381,7 +382,9 @@ class AppleTVConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
CONF_IDENTIFIERS: list(combined_identifiers),
|
||||
},
|
||||
)
|
||||
if entry.source != SOURCE_IGNORE:
|
||||
# Don't reload ignored entries or in the middle of reauth,
|
||||
# e.g. if the user is entering a new PIN
|
||||
if entry.source != SOURCE_IGNORE and self.source != SOURCE_REAUTH:
|
||||
self.hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
if not allow_exist:
|
||||
raise DeviceAlreadyConfigured
|
||||
|
@ -1,9 +1,11 @@
|
||||
"""Base class for assist satellite entities."""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.http import StaticPathConfig
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
@ -15,6 +17,8 @@ from .const import (
|
||||
CONNECTION_TEST_DATA,
|
||||
DATA_COMPONENT,
|
||||
DOMAIN,
|
||||
PREANNOUNCE_FILENAME,
|
||||
PREANNOUNCE_URL,
|
||||
AssistSatelliteEntityFeature,
|
||||
)
|
||||
from .entity import (
|
||||
@ -56,6 +60,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
{
|
||||
vol.Optional("message"): str,
|
||||
vol.Optional("media_id"): str,
|
||||
vol.Optional("preannounce"): bool,
|
||||
vol.Optional("preannounce_media_id"): str,
|
||||
}
|
||||
),
|
||||
@ -71,6 +76,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
{
|
||||
vol.Optional("start_message"): str,
|
||||
vol.Optional("start_media_id"): str,
|
||||
vol.Optional("preannounce"): bool,
|
||||
vol.Optional("preannounce_media_id"): str,
|
||||
vol.Optional("extra_system_prompt"): str,
|
||||
}
|
||||
@ -84,6 +90,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
async_register_websocket_api(hass)
|
||||
hass.http.register_view(ConnectionTestView())
|
||||
|
||||
# Default preannounce sound
|
||||
await hass.http.async_register_static_paths(
|
||||
[
|
||||
StaticPathConfig(
|
||||
PREANNOUNCE_URL, str(Path(__file__).parent / PREANNOUNCE_FILENAME)
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
@ -20,6 +20,9 @@ CONNECTION_TEST_DATA: HassKey[dict[str, asyncio.Event]] = HassKey(
|
||||
f"{DOMAIN}_connection_tests"
|
||||
)
|
||||
|
||||
PREANNOUNCE_FILENAME = "preannounce.mp3"
|
||||
PREANNOUNCE_URL = f"/api/assist_satellite/static/{PREANNOUNCE_FILENAME}"
|
||||
|
||||
|
||||
class AssistSatelliteEntityFeature(IntFlag):
|
||||
"""Supported features of Assist satellite entity."""
|
||||
|
@ -28,7 +28,7 @@ from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import chat_session, entity
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
|
||||
from .const import AssistSatelliteEntityFeature
|
||||
from .const import PREANNOUNCE_URL, AssistSatelliteEntityFeature
|
||||
from .errors import AssistSatelliteError, SatelliteBusyError
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -180,7 +180,8 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
self,
|
||||
message: str | None = None,
|
||||
media_id: str | None = None,
|
||||
preannounce_media_id: str | None = None,
|
||||
preannounce: bool = True,
|
||||
preannounce_media_id: str = PREANNOUNCE_URL,
|
||||
) -> None:
|
||||
"""Play and show an announcement on the satellite.
|
||||
|
||||
@ -190,7 +191,8 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
If media_id is provided, it is played directly. It is possible
|
||||
to omit the message and the satellite will not show any text.
|
||||
|
||||
If preannounce_media_id is provided, it is played before the announcement.
|
||||
If preannounce is True, a sound is played before the announcement.
|
||||
If preannounce_media_id is provided, it overrides the default sound.
|
||||
|
||||
Calls async_announce with message and media id.
|
||||
"""
|
||||
@ -200,7 +202,9 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
message = ""
|
||||
|
||||
announcement = await self._resolve_announcement_media_id(
|
||||
message, media_id, preannounce_media_id
|
||||
message,
|
||||
media_id,
|
||||
preannounce_media_id=preannounce_media_id if preannounce else None,
|
||||
)
|
||||
|
||||
if self._is_announcing:
|
||||
@ -228,7 +232,8 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
start_message: str | None = None,
|
||||
start_media_id: str | None = None,
|
||||
extra_system_prompt: str | None = None,
|
||||
preannounce_media_id: str | None = None,
|
||||
preannounce: bool = True,
|
||||
preannounce_media_id: str = PREANNOUNCE_URL,
|
||||
) -> None:
|
||||
"""Start a conversation from the satellite.
|
||||
|
||||
@ -238,7 +243,8 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
If start_media_id is provided, it is played directly. It is possible
|
||||
to omit the message and the satellite will not show any text.
|
||||
|
||||
If preannounce_media_id is provided, it is played before the announcement.
|
||||
If preannounce is True, a sound is played before the start message or media.
|
||||
If preannounce_media_id is provided, it overrides the default sound.
|
||||
|
||||
Calls async_start_conversation.
|
||||
"""
|
||||
@ -255,7 +261,9 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
start_message = ""
|
||||
|
||||
announcement = await self._resolve_announcement_media_id(
|
||||
start_message, start_media_id, preannounce_media_id
|
||||
start_message,
|
||||
start_media_id,
|
||||
preannounce_media_id=preannounce_media_id if preannounce else None,
|
||||
)
|
||||
|
||||
if self._is_announcing:
|
||||
|
BIN
homeassistant/components/assist_satellite/preannounce.mp3
Normal file
BIN
homeassistant/components/assist_satellite/preannounce.mp3
Normal file
Binary file not shown.
@ -8,12 +8,18 @@ announce:
|
||||
message:
|
||||
required: false
|
||||
example: "Time to wake up!"
|
||||
default: ""
|
||||
selector:
|
||||
text:
|
||||
media_id:
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
preannounce:
|
||||
required: false
|
||||
default: true
|
||||
selector:
|
||||
boolean:
|
||||
preannounce_media_id:
|
||||
required: false
|
||||
selector:
|
||||
@ -28,6 +34,7 @@ start_conversation:
|
||||
start_message:
|
||||
required: false
|
||||
example: "You left the lights on in the living room. Turn them off?"
|
||||
default: ""
|
||||
selector:
|
||||
text:
|
||||
start_media_id:
|
||||
@ -38,6 +45,11 @@ start_conversation:
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
preannounce:
|
||||
required: false
|
||||
default: true
|
||||
selector:
|
||||
boolean:
|
||||
preannounce_media_id:
|
||||
required: false
|
||||
selector:
|
||||
|
@ -24,9 +24,13 @@
|
||||
"name": "Media ID",
|
||||
"description": "The media ID to announce instead of using text-to-speech."
|
||||
},
|
||||
"preannounce": {
|
||||
"name": "Preannounce",
|
||||
"description": "Play a sound before the announcement."
|
||||
},
|
||||
"preannounce_media_id": {
|
||||
"name": "Preannounce Media ID",
|
||||
"description": "The media ID to play before the announcement."
|
||||
"name": "Preannounce media ID",
|
||||
"description": "Custom media ID to play before the announcement."
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -46,9 +50,13 @@
|
||||
"name": "Extra system prompt",
|
||||
"description": "Provide background information to the AI about the request."
|
||||
},
|
||||
"preannounce": {
|
||||
"name": "Preannounce",
|
||||
"description": "Play a sound before the start message or media."
|
||||
},
|
||||
"preannounce_media_id": {
|
||||
"name": "Preannounce Media ID",
|
||||
"description": "The media ID to play before the start message or media."
|
||||
"name": "Preannounce media ID",
|
||||
"description": "Custom media ID to play before the start message or media."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -198,7 +198,8 @@ async def websocket_test_connection(
|
||||
|
||||
hass.async_create_background_task(
|
||||
satellite.async_internal_announce(
|
||||
media_id=f"{CONNECTION_TEST_URL_BASE}/{connection_id}"
|
||||
media_id=f"{CONNECTION_TEST_URL_BASE}/{connection_id}",
|
||||
preannounce=False,
|
||||
),
|
||||
f"assist_satellite_connection_test_{msg['entity_id']}",
|
||||
)
|
||||
|
@ -175,7 +175,8 @@ class AzureStorageBackupAgent(BackupAgent):
|
||||
"""Find a blob by backup id."""
|
||||
async for blob in self._client.list_blobs(include="metadata"):
|
||||
if (
|
||||
backup_id == blob.metadata.get("backup_id", "")
|
||||
blob.metadata is not None
|
||||
and backup_id == blob.metadata.get("backup_id", "")
|
||||
and blob.metadata.get("metadata_version") == METADATA_VERSION
|
||||
):
|
||||
return blob
|
||||
|
@ -501,18 +501,16 @@ class BluesoundPlayer(CoordinatorEntity[BluesoundCoordinator], MediaPlayerEntity
|
||||
return
|
||||
|
||||
# presets and inputs might have the same name; presets have priority
|
||||
url: str | None = None
|
||||
for input_ in self._inputs:
|
||||
if input_.text == source:
|
||||
url = input_.url
|
||||
await self._player.play_url(input_.url)
|
||||
return
|
||||
for preset in self._presets:
|
||||
if preset.name == source:
|
||||
url = preset.url
|
||||
await self._player.load_preset(preset.id)
|
||||
return
|
||||
|
||||
if url is None:
|
||||
raise ServiceValidationError(f"Source {source} not found")
|
||||
|
||||
await self._player.play_url(url)
|
||||
raise ServiceValidationError(f"Source {source} not found")
|
||||
|
||||
async def async_clear_playlist(self) -> None:
|
||||
"""Clear players playlist."""
|
||||
|
@ -19,7 +19,7 @@
|
||||
"bleak-retry-connector==3.9.0",
|
||||
"bluetooth-adapters==0.21.4",
|
||||
"bluetooth-auto-recovery==1.4.5",
|
||||
"bluetooth-data-tools==1.26.1",
|
||||
"bluetooth-data-tools==1.26.5",
|
||||
"dbus-fast==2.43.0",
|
||||
"habluetooth==3.37.0"
|
||||
]
|
||||
|
@ -127,7 +127,11 @@ class CloudOAuth2Implementation(config_entry_oauth2_flow.AbstractOAuth2Implement
|
||||
flow_id=flow_id, user_input=tokens
|
||||
)
|
||||
|
||||
self.hass.async_create_task(await_tokens())
|
||||
# It's a background task because it should be cancelled on shutdown and there's nothing else
|
||||
# we can do in such case. There's also no need to wait for this during setup.
|
||||
self.hass.async_create_background_task(
|
||||
await_tokens(), name="Awaiting OAuth tokens"
|
||||
)
|
||||
|
||||
return authorize_url
|
||||
|
||||
|
@ -4,13 +4,14 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import AsyncIterator, Callable, Coroutine, Mapping
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
import random
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import ClientError
|
||||
from aiohttp import ClientError, ClientResponseError
|
||||
from hass_nabucasa import Cloud, CloudError
|
||||
from hass_nabucasa.api import CloudApiNonRetryableError
|
||||
from hass_nabucasa.api import CloudApiError, CloudApiNonRetryableError
|
||||
from hass_nabucasa.cloud_api import (
|
||||
FilesHandlerListEntry,
|
||||
async_files_delete_file,
|
||||
@ -120,6 +121,8 @@ class CloudBackupAgent(BackupAgent):
|
||||
"""
|
||||
if not backup.protected:
|
||||
raise BackupAgentError("Cloud backups must be protected")
|
||||
if self._cloud.subscription_expired:
|
||||
raise BackupAgentError("Cloud subscription has expired")
|
||||
|
||||
size = backup.size
|
||||
try:
|
||||
@ -152,6 +155,13 @@ class CloudBackupAgent(BackupAgent):
|
||||
) from err
|
||||
raise BackupAgentError(f"Failed to upload backup {err}") from err
|
||||
except CloudError as err:
|
||||
if (
|
||||
isinstance(err, CloudApiError)
|
||||
and isinstance(err.orig_exc, ClientResponseError)
|
||||
and err.orig_exc.status == HTTPStatus.FORBIDDEN
|
||||
and self._cloud.subscription_expired
|
||||
):
|
||||
raise BackupAgentError("Cloud subscription has expired") from err
|
||||
if tries == _RETRY_LIMIT:
|
||||
raise BackupAgentError(f"Failed to upload backup {err}") from err
|
||||
tries += 1
|
||||
|
@ -41,6 +41,7 @@ ALARM_ACTIONS: dict[str, str] = {
|
||||
|
||||
|
||||
ALARM_AREA_ARMED_STATUS: dict[str, int] = {
|
||||
DISABLE: 0,
|
||||
HOME_P1: 1,
|
||||
HOME_P2: 2,
|
||||
NIGHT: 3,
|
||||
@ -128,20 +129,38 @@ class ComelitAlarmEntity(CoordinatorEntity[ComelitVedoSystem], AlarmControlPanel
|
||||
AlarmAreaState.TRIGGERED: AlarmControlPanelState.TRIGGERED,
|
||||
}.get(self._area.human_status)
|
||||
|
||||
async def _async_update_state(self, area_state: AlarmAreaState, armed: int) -> None:
|
||||
"""Update state after action."""
|
||||
self._area.human_status = area_state
|
||||
self._area.armed = armed
|
||||
await self.async_update_ha_state()
|
||||
|
||||
async def async_alarm_disarm(self, code: str | None = None) -> None:
|
||||
"""Send disarm command."""
|
||||
if code != str(self._api.device_pin):
|
||||
return
|
||||
await self._api.set_zone_status(self._area.index, ALARM_ACTIONS[DISABLE])
|
||||
await self._async_update_state(
|
||||
AlarmAreaState.DISARMED, ALARM_AREA_ARMED_STATUS[DISABLE]
|
||||
)
|
||||
|
||||
async def async_alarm_arm_away(self, code: str | None = None) -> None:
|
||||
"""Send arm away command."""
|
||||
await self._api.set_zone_status(self._area.index, ALARM_ACTIONS[AWAY])
|
||||
await self._async_update_state(
|
||||
AlarmAreaState.ARMED, ALARM_AREA_ARMED_STATUS[AWAY]
|
||||
)
|
||||
|
||||
async def async_alarm_arm_home(self, code: str | None = None) -> None:
|
||||
"""Send arm home command."""
|
||||
await self._api.set_zone_status(self._area.index, ALARM_ACTIONS[HOME])
|
||||
await self._async_update_state(
|
||||
AlarmAreaState.ARMED, ALARM_AREA_ARMED_STATUS[HOME_P1]
|
||||
)
|
||||
|
||||
async def async_alarm_arm_night(self, code: str | None = None) -> None:
|
||||
"""Send arm night command."""
|
||||
await self._api.set_zone_status(self._area.index, ALARM_ACTIONS[NIGHT])
|
||||
await self._async_update_state(
|
||||
AlarmAreaState.ARMED, ALARM_AREA_ARMED_STATUS[NIGHT]
|
||||
)
|
||||
|
@ -8,7 +8,7 @@ from aiocomelit import ComelitSerialBridgeObject
|
||||
from aiocomelit.const import COVER, STATE_COVER, STATE_OFF, STATE_ON
|
||||
|
||||
from homeassistant.components.cover import CoverDeviceClass, CoverEntity, CoverState
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
@ -98,13 +98,20 @@ class ComelitCoverEntity(
|
||||
"""Return if the cover is opening."""
|
||||
return self._current_action("opening")
|
||||
|
||||
async def _cover_set_state(self, action: int, state: int) -> None:
|
||||
"""Set desired cover state."""
|
||||
self._last_state = self.state
|
||||
await self._api.set_device_status(COVER, self._device.index, action)
|
||||
self.coordinator.data[COVER][self._device.index].status = state
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_close_cover(self, **kwargs: Any) -> None:
|
||||
"""Close cover."""
|
||||
await self._api.set_device_status(COVER, self._device.index, STATE_OFF)
|
||||
await self._cover_set_state(STATE_OFF, 2)
|
||||
|
||||
async def async_open_cover(self, **kwargs: Any) -> None:
|
||||
"""Open cover."""
|
||||
await self._api.set_device_status(COVER, self._device.index, STATE_ON)
|
||||
await self._cover_set_state(STATE_ON, 1)
|
||||
|
||||
async def async_stop_cover(self, **_kwargs: Any) -> None:
|
||||
"""Stop the cover."""
|
||||
@ -112,13 +119,7 @@ class ComelitCoverEntity(
|
||||
return
|
||||
|
||||
action = STATE_ON if self.is_closing else STATE_OFF
|
||||
await self._api.set_device_status(COVER, self._device.index, action)
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle device update."""
|
||||
self._last_state = self.state
|
||||
self.async_write_ha_state()
|
||||
await self._cover_set_state(action, 0)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Handle entity which will be added."""
|
||||
|
@ -162,7 +162,7 @@ class ComelitHumidifierEntity(CoordinatorEntity[ComelitSerialBridge], Humidifier
|
||||
|
||||
async def async_set_humidity(self, humidity: int) -> None:
|
||||
"""Set new target humidity."""
|
||||
if self.mode == HumidifierComelitMode.OFF:
|
||||
if not self._attr_is_on:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="humidity_while_off",
|
||||
@ -190,9 +190,13 @@ class ComelitHumidifierEntity(CoordinatorEntity[ComelitSerialBridge], Humidifier
|
||||
await self.coordinator.api.set_humidity_status(
|
||||
self._device.index, self._set_command
|
||||
)
|
||||
self._attr_is_on = True
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off."""
|
||||
await self.coordinator.api.set_humidity_status(
|
||||
self._device.index, HumidifierComelitCommand.OFF
|
||||
)
|
||||
self._attr_is_on = False
|
||||
self.async_write_ha_state()
|
||||
|
@ -59,7 +59,8 @@ class ComelitLightEntity(CoordinatorEntity[ComelitSerialBridge], LightEntity):
|
||||
async def _light_set_state(self, state: int) -> None:
|
||||
"""Set desired light state."""
|
||||
await self.coordinator.api.set_device_status(LIGHT, self._device.index, state)
|
||||
await self.coordinator.async_request_refresh()
|
||||
self.coordinator.data[LIGHT][self._device.index].status = state
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the light on."""
|
||||
|
@ -52,7 +52,9 @@
|
||||
"rest": "Rest",
|
||||
"sabotated": "Sabotated"
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
"humidifier": {
|
||||
"humidifier": {
|
||||
"name": "Humidifier"
|
||||
},
|
||||
|
@ -67,7 +67,8 @@ class ComelitSwitchEntity(CoordinatorEntity[ComelitSerialBridge], SwitchEntity):
|
||||
await self.coordinator.api.set_device_status(
|
||||
self._device.type, self._device.index, state
|
||||
)
|
||||
await self.coordinator.async_request_refresh()
|
||||
self.coordinator.data[self._device.type][self._device.index].status = state
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch on."""
|
||||
|
@ -650,7 +650,14 @@ class DefaultAgent(ConversationEntity):
|
||||
|
||||
if (
|
||||
(maybe_result is None) # first result
|
||||
or (num_matched_entities > best_num_matched_entities)
|
||||
or (
|
||||
# More literal text matched
|
||||
result.text_chunks_matched > maybe_result.text_chunks_matched
|
||||
)
|
||||
or (
|
||||
# More entities matched
|
||||
num_matched_entities > best_num_matched_entities
|
||||
)
|
||||
or (
|
||||
# Fewer unmatched entities
|
||||
(num_matched_entities == best_num_matched_entities)
|
||||
@ -662,16 +669,6 @@ class DefaultAgent(ConversationEntity):
|
||||
and (num_unmatched_entities == best_num_unmatched_entities)
|
||||
and (num_unmatched_ranges > best_num_unmatched_ranges)
|
||||
)
|
||||
or (
|
||||
# More literal text matched
|
||||
(num_matched_entities == best_num_matched_entities)
|
||||
and (num_unmatched_entities == best_num_unmatched_entities)
|
||||
and (num_unmatched_ranges == best_num_unmatched_ranges)
|
||||
and (
|
||||
result.text_chunks_matched
|
||||
> maybe_result.text_chunks_matched
|
||||
)
|
||||
)
|
||||
or (
|
||||
# Prefer match failures with entities
|
||||
(result.text_chunks_matched == maybe_result.text_chunks_matched)
|
||||
|
@ -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.3.24"]
|
||||
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.3.28"]
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
|
||||
from homeassistant.util.ssl import client_context_no_verify
|
||||
|
||||
from .const import KEY_MAC, TIMEOUT
|
||||
from .coordinator import DaikinConfigEntry, DaikinCoordinator
|
||||
@ -48,6 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: DaikinConfigEntry) -> bo
|
||||
key=entry.data.get(CONF_API_KEY),
|
||||
uuid=entry.data.get(CONF_UUID),
|
||||
password=entry.data.get(CONF_PASSWORD),
|
||||
ssl_context=client_context_no_verify(),
|
||||
)
|
||||
_LOGGER.debug("Connection to %s successful", host)
|
||||
except TimeoutError as err:
|
||||
|
@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/daikin",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pydaikin"],
|
||||
"requirements": ["pydaikin==2.14.1"],
|
||||
"requirements": ["pydaikin==2.15.0"],
|
||||
"zeroconf": ["_dkapi._tcp.local."]
|
||||
}
|
||||
|
@ -8,7 +8,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/dlna_dmr",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["async_upnp_client"],
|
||||
"requirements": ["async-upnp-client==0.43.0", "getmac==0.9.5"],
|
||||
"requirements": ["async-upnp-client==0.44.0", "getmac==0.9.5"],
|
||||
"ssdp": [
|
||||
{
|
||||
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",
|
||||
|
@ -7,7 +7,7 @@
|
||||
"dependencies": ["ssdp"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/dlna_dms",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["async-upnp-client==0.43.0"],
|
||||
"requirements": ["async-upnp-client==0.44.0"],
|
||||
"ssdp": [
|
||||
{
|
||||
"deviceType": "urn:schemas-upnp-org:device:MediaServer:1",
|
||||
|
@ -50,10 +50,10 @@ class DukeEnergyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
username = auth["cdp_internal_user_id"].lower()
|
||||
username = auth["internalUserID"].lower()
|
||||
await self.async_set_unique_id(username)
|
||||
self._abort_if_unique_id_configured()
|
||||
email = auth["email"].lower()
|
||||
email = auth["loginEmailAddress"].lower()
|
||||
data = {
|
||||
CONF_EMAIL: email,
|
||||
CONF_USERNAME: username,
|
||||
|
@ -6,5 +6,5 @@
|
||||
"dependencies": ["recorder"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/duke_energy",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["aiodukeenergy==0.2.2"]
|
||||
"requirements": ["aiodukeenergy==0.3.0"]
|
||||
}
|
||||
|
@ -91,15 +91,15 @@ class EcoNetWaterHeater(EcoNetEntity[WaterHeater], WaterHeaterEntity):
|
||||
def operation_list(self) -> list[str]:
|
||||
"""List of available operation modes."""
|
||||
econet_modes = self.water_heater.modes
|
||||
op_list = []
|
||||
operation_modes = set()
|
||||
for mode in econet_modes:
|
||||
if (
|
||||
mode is not WaterHeaterOperationMode.UNKNOWN
|
||||
and mode is not WaterHeaterOperationMode.VACATION
|
||||
):
|
||||
ha_mode = ECONET_STATE_TO_HA[mode]
|
||||
op_list.append(ha_mode)
|
||||
return op_list
|
||||
operation_modes.add(ha_mode)
|
||||
return list(operation_modes)
|
||||
|
||||
@property
|
||||
def supported_features(self) -> WaterHeaterEntityFeature:
|
||||
|
@ -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==12.3.1"]
|
||||
"requirements": ["py-sucks==0.9.10", "deebot-client==12.5.0"]
|
||||
}
|
||||
|
@ -100,7 +100,11 @@ class ElkEntity(Entity):
|
||||
return {"index": self._element.index + 1}
|
||||
|
||||
def _element_changed(self, element: Element, changeset: dict[str, Any]) -> None:
|
||||
pass
|
||||
"""Handle changes to the element.
|
||||
|
||||
This method is called when the element changes. It should be
|
||||
overridden by subclasses to handle the changes.
|
||||
"""
|
||||
|
||||
@callback
|
||||
def _element_callback(self, element: Element, changeset: dict[str, Any]) -> None:
|
||||
@ -111,7 +115,7 @@ class ElkEntity(Entity):
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callback for ElkM1 changes and update entity state."""
|
||||
self._element.add_callback(self._element_callback)
|
||||
self._element_callback(self._element, {})
|
||||
self._element_changed(self._element, {})
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
|
@ -6,7 +6,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/enphase_envoy",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyenphase"],
|
||||
"requirements": ["pyenphase==1.25.1"],
|
||||
"requirements": ["pyenphase==1.25.5"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_enphase-envoy._tcp.local."
|
||||
|
@ -128,8 +128,23 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
self._password = ""
|
||||
return await self._async_authenticate_or_add()
|
||||
|
||||
if error is None and entry_data.get(CONF_NOISE_PSK):
|
||||
return await self.async_step_reauth_encryption_removed_confirm()
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_encryption_removed_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reauthorization flow when encryption was removed."""
|
||||
if user_input is not None:
|
||||
self._noise_psk = None
|
||||
return self._async_get_entry()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reauth_encryption_removed_confirm",
|
||||
description_placeholders={"name": self._name},
|
||||
)
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
@ -282,15 +282,18 @@ class RuntimeEntryData:
|
||||
) -> None:
|
||||
"""Distribute an update of static infos to all platforms."""
|
||||
# First, load all platforms
|
||||
needed_platforms = set()
|
||||
if async_get_dashboard(hass):
|
||||
needed_platforms.add(Platform.UPDATE)
|
||||
needed_platforms: set[Platform] = set()
|
||||
|
||||
if self.device_info and self.device_info.voice_assistant_feature_flags_compat(
|
||||
self.api_version
|
||||
):
|
||||
needed_platforms.add(Platform.BINARY_SENSOR)
|
||||
needed_platforms.add(Platform.SELECT)
|
||||
if self.device_info:
|
||||
if async_get_dashboard(hass):
|
||||
# Only load the update platform if the device_info is set
|
||||
# When we restore the entry, the device_info may not be set yet
|
||||
# and we don't want to load the update platform since it needs
|
||||
# a complete device_info.
|
||||
needed_platforms.add(Platform.UPDATE)
|
||||
if self.device_info.voice_assistant_feature_flags_compat(self.api_version):
|
||||
needed_platforms.add(Platform.BINARY_SENSOR)
|
||||
needed_platforms.add(Platform.SELECT)
|
||||
|
||||
ent_reg = er.async_get(hass)
|
||||
registry_get_entity = ent_reg.async_get_entity_id
|
||||
@ -312,18 +315,19 @@ class RuntimeEntryData:
|
||||
|
||||
# Make a dict of the EntityInfo by type and send
|
||||
# them to the listeners for each specific EntityInfo type
|
||||
infos_by_type: dict[type[EntityInfo], list[EntityInfo]] = {}
|
||||
infos_by_type: defaultdict[type[EntityInfo], list[EntityInfo]] = defaultdict(
|
||||
list
|
||||
)
|
||||
for info in infos:
|
||||
info_type = type(info)
|
||||
if info_type not in infos_by_type:
|
||||
infos_by_type[info_type] = []
|
||||
infos_by_type[info_type].append(info)
|
||||
infos_by_type[type(info)].append(info)
|
||||
|
||||
callbacks_by_type = self.entity_info_callbacks
|
||||
for type_, entity_infos in infos_by_type.items():
|
||||
if callbacks_ := callbacks_by_type.get(type_):
|
||||
for callback_ in callbacks_:
|
||||
callback_(entity_infos)
|
||||
for type_, callbacks in self.entity_info_callbacks.items():
|
||||
# If all entities for a type are removed, we
|
||||
# still need to call the callbacks with an empty list
|
||||
# to make sure the entities are removed.
|
||||
entity_infos = infos_by_type.get(type_, [])
|
||||
for callback_ in callbacks:
|
||||
callback_(entity_infos)
|
||||
|
||||
# Finally update static info subscriptions
|
||||
for callback_ in self.static_info_update_subscriptions:
|
||||
|
@ -33,6 +33,16 @@ class EsphomeEvent(EsphomeEntity[EventInfo, Event], EventEntity):
|
||||
self._trigger_event(self._state.event_type)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _on_device_update(self) -> None:
|
||||
"""Call when device updates or entry data changes."""
|
||||
super()._on_device_update()
|
||||
if self._entry_data.available:
|
||||
# Event entities should go available directly
|
||||
# when the device comes online and not wait
|
||||
# for the next data push.
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
async_setup_entry = partial(
|
||||
platform_async_setup_entry,
|
||||
|
@ -13,6 +13,7 @@ from aioesphomeapi import (
|
||||
APIConnectionError,
|
||||
APIVersion,
|
||||
DeviceInfo as EsphomeDeviceInfo,
|
||||
EncryptionHelloAPIError,
|
||||
EntityInfo,
|
||||
HomeassistantServiceCall,
|
||||
InvalidAuthAPIError,
|
||||
@ -570,6 +571,7 @@ class ESPHomeManager:
|
||||
if isinstance(
|
||||
err,
|
||||
(
|
||||
EncryptionHelloAPIError,
|
||||
RequiresEncryptionAPIError,
|
||||
InvalidEncryptionKeyAPIError,
|
||||
InvalidAuthAPIError,
|
||||
|
@ -16,7 +16,7 @@
|
||||
"loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"],
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"requirements": [
|
||||
"aioesphomeapi==29.7.0",
|
||||
"aioesphomeapi==29.8.0",
|
||||
"esphome-dashboard-api==1.2.3",
|
||||
"bleak-esphome==2.12.0"
|
||||
],
|
||||
|
@ -43,6 +43,9 @@
|
||||
},
|
||||
"description": "The ESPHome device {name} enabled transport encryption or changed the encryption key. Please enter the updated key. You can find it in the ESPHome Dashboard or in your device configuration."
|
||||
},
|
||||
"reauth_encryption_removed_confirm": {
|
||||
"description": "The ESPHome device {name} disabled transport encryption. Please confirm that you want to remove the encryption key and allow unencrypted connections."
|
||||
},
|
||||
"discovery_confirm": {
|
||||
"description": "Do you want to add the ESPHome node `{name}` to Home Assistant?",
|
||||
"title": "Discovered ESPHome node"
|
||||
|
@ -6,5 +6,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["evohome", "evohomeasync", "evohomeasync2"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["evohome-async==1.0.4"]
|
||||
"requirements": ["evohome-async==1.0.5"]
|
||||
}
|
||||
|
@ -301,6 +301,7 @@ class FibaroController:
|
||||
device.ha_id = (
|
||||
f"{slugify(room_name)}_{slugify(device.name)}_{device.fibaro_id}"
|
||||
)
|
||||
platform = None
|
||||
if device.enabled and (not device.is_plugin or self._import_plugins):
|
||||
platform = self._map_device_to_platform(device)
|
||||
if platform is None:
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/forecast_solar",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["forecast-solar==4.0.0"]
|
||||
"requirements": ["forecast-solar==4.1.0"]
|
||||
}
|
||||
|
@ -193,7 +193,6 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = (
|
||||
translation_key="max_kb_s_sent",
|
||||
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
|
||||
device_class=SensorDeviceClass.DATA_RATE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=_retrieve_max_kb_s_sent_state,
|
||||
),
|
||||
FritzSensorEntityDescription(
|
||||
@ -201,7 +200,6 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = (
|
||||
translation_key="max_kb_s_received",
|
||||
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
|
||||
device_class=SensorDeviceClass.DATA_RATE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=_retrieve_max_kb_s_received_state,
|
||||
),
|
||||
FritzSensorEntityDescription(
|
||||
@ -225,6 +223,7 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = (
|
||||
translation_key="link_kb_s_sent",
|
||||
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
|
||||
device_class=SensorDeviceClass.DATA_RATE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=_retrieve_link_kb_s_sent_state,
|
||||
),
|
||||
FritzSensorEntityDescription(
|
||||
@ -232,12 +231,15 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = (
|
||||
translation_key="link_kb_s_received",
|
||||
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
|
||||
device_class=SensorDeviceClass.DATA_RATE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=_retrieve_link_kb_s_received_state,
|
||||
),
|
||||
FritzSensorEntityDescription(
|
||||
key="link_noise_margin_sent",
|
||||
translation_key="link_noise_margin_sent",
|
||||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=_retrieve_link_noise_margin_sent_state,
|
||||
is_suitable=lambda info: info.wan_enabled and info.connection == DSL_CONNECTION,
|
||||
),
|
||||
@ -245,6 +247,8 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = (
|
||||
key="link_noise_margin_received",
|
||||
translation_key="link_noise_margin_received",
|
||||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=_retrieve_link_noise_margin_received_state,
|
||||
is_suitable=lambda info: info.wan_enabled and info.connection == DSL_CONNECTION,
|
||||
),
|
||||
@ -252,6 +256,8 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = (
|
||||
key="link_attenuation_sent",
|
||||
translation_key="link_attenuation_sent",
|
||||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=_retrieve_link_attenuation_sent_state,
|
||||
is_suitable=lambda info: info.wan_enabled and info.connection == DSL_CONNECTION,
|
||||
),
|
||||
@ -259,6 +265,8 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = (
|
||||
key="link_attenuation_received",
|
||||
translation_key="link_attenuation_received",
|
||||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=_retrieve_link_attenuation_received_state,
|
||||
is_suitable=lambda info: info.wan_enabled and info.connection == DSL_CONNECTION,
|
||||
),
|
||||
|
@ -6,6 +6,7 @@ from typing import Any
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_HVAC_MODE,
|
||||
PRESET_BOOST,
|
||||
PRESET_COMFORT,
|
||||
PRESET_ECO,
|
||||
ClimateEntity,
|
||||
@ -38,7 +39,7 @@ from .sensor import value_scheduled_preset
|
||||
HVAC_MODES = [HVACMode.HEAT, HVACMode.OFF]
|
||||
PRESET_HOLIDAY = "holiday"
|
||||
PRESET_SUMMER = "summer"
|
||||
PRESET_MODES = [PRESET_ECO, PRESET_COMFORT]
|
||||
PRESET_MODES = [PRESET_ECO, PRESET_COMFORT, PRESET_BOOST]
|
||||
SUPPORTED_FEATURES = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.PRESET_MODE
|
||||
@ -194,6 +195,8 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
||||
return PRESET_HOLIDAY
|
||||
if self.data.summer_active:
|
||||
return PRESET_SUMMER
|
||||
if self.data.target_temperature == ON_API_TEMPERATURE:
|
||||
return PRESET_BOOST
|
||||
if self.data.target_temperature == self.data.comfort_temperature:
|
||||
return PRESET_COMFORT
|
||||
if self.data.target_temperature == self.data.eco_temperature:
|
||||
@ -211,6 +214,8 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
|
||||
await self.async_set_temperature(temperature=self.data.comfort_temperature)
|
||||
elif preset_mode == PRESET_ECO:
|
||||
await self.async_set_temperature(temperature=self.data.eco_temperature)
|
||||
elif preset_mode == PRESET_BOOST:
|
||||
await self.async_set_temperature(temperature=ON_REPORT_SET_TEMPERATURE)
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> ClimateExtraAttributes:
|
||||
|
@ -137,6 +137,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = (
|
||||
key="battery",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
suitable=lambda device: device.battery_level is not None,
|
||||
native_value=lambda device: device.battery_level,
|
||||
|
@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20250326.0"]
|
||||
"requirements": ["home-assistant-frontend==20250404.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.0.1"]
|
||||
"requirements": ["gcal-sync==7.0.0", "oauth2client==4.1.3", "ical==9.0.3"]
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
import mimetypes
|
||||
from pathlib import Path
|
||||
|
||||
from google import genai # type: ignore[attr-defined]
|
||||
from google.genai import Client
|
||||
from google.genai.errors import APIError, ClientError
|
||||
from requests.exceptions import Timeout
|
||||
import voluptuous as vol
|
||||
@ -43,7 +43,7 @@ CONF_FILENAMES = "filenames"
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
PLATFORMS = (Platform.CONVERSATION,)
|
||||
|
||||
type GoogleGenerativeAIConfigEntry = ConfigEntry[genai.Client]
|
||||
type GoogleGenerativeAIConfigEntry = ConfigEntry[Client]
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
@ -139,7 +139,11 @@ async def async_setup_entry(
|
||||
"""Set up Google Generative AI Conversation from a config entry."""
|
||||
|
||||
try:
|
||||
client = genai.Client(api_key=entry.data[CONF_API_KEY])
|
||||
|
||||
def _init_client() -> Client:
|
||||
return Client(api_key=entry.data[CONF_API_KEY])
|
||||
|
||||
client = await hass.async_add_executor_job(_init_client)
|
||||
await client.aio.models.get(
|
||||
model=entry.options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL),
|
||||
config={"http_options": {"timeout": TIMEOUT_MILLIS}},
|
||||
|
@ -7,7 +7,7 @@ import logging
|
||||
from types import MappingProxyType
|
||||
from typing import Any
|
||||
|
||||
from google import genai # type: ignore[attr-defined]
|
||||
from google import genai
|
||||
from google.genai.errors import APIError, ClientError
|
||||
from requests.exceptions import Timeout
|
||||
import voluptuous as vol
|
||||
@ -179,28 +179,30 @@ class GoogleGenerativeAIOptionsFlow(OptionsFlow):
|
||||
) -> ConfigFlowResult:
|
||||
"""Manage the options."""
|
||||
options: dict[str, Any] | MappingProxyType[str, Any] = self.config_entry.options
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
if user_input[CONF_RECOMMENDED] == self.last_rendered_recommended:
|
||||
if user_input[CONF_LLM_HASS_API] == "none":
|
||||
user_input.pop(CONF_LLM_HASS_API)
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
if not (
|
||||
user_input.get(CONF_LLM_HASS_API, "none") != "none"
|
||||
and user_input.get(CONF_USE_GOOGLE_SEARCH_TOOL, False) is True
|
||||
):
|
||||
# Don't allow to save options that enable the Google Seearch tool with an Assist API
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
errors[CONF_USE_GOOGLE_SEARCH_TOOL] = "invalid_google_search_option"
|
||||
|
||||
# Re-render the options again, now with the recommended options shown/hidden
|
||||
self.last_rendered_recommended = user_input[CONF_RECOMMENDED]
|
||||
|
||||
options = {
|
||||
CONF_RECOMMENDED: user_input[CONF_RECOMMENDED],
|
||||
CONF_PROMPT: user_input[CONF_PROMPT],
|
||||
CONF_LLM_HASS_API: user_input[CONF_LLM_HASS_API],
|
||||
}
|
||||
options = user_input
|
||||
|
||||
schema = await google_generative_ai_config_option_schema(
|
||||
self.hass, options, self._genai_client
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=vol.Schema(schema),
|
||||
step_id="init", data_schema=vol.Schema(schema), errors=errors
|
||||
)
|
||||
|
||||
|
||||
|
@ -356,6 +356,15 @@ class GoogleGenerativeAIConversationEntity(
|
||||
|
||||
messages.append(_convert_content(chat_content))
|
||||
|
||||
# The SDK requires the first message to be a user message
|
||||
# This is not the case if user used `start_conversation`
|
||||
# Workaround from https://github.com/googleapis/python-genai/issues/529#issuecomment-2740964537
|
||||
if messages and messages[0].role != "user":
|
||||
messages.insert(
|
||||
0,
|
||||
Content(role="user", parts=[Part.from_text(text=" ")]),
|
||||
)
|
||||
|
||||
if tool_results:
|
||||
messages.append(_create_google_tool_response_content(tool_results))
|
||||
generateContentConfig = GenerateContentConfig(
|
||||
|
@ -43,6 +43,9 @@
|
||||
"prompt": "Instruct how the LLM should respond. This can be a template."
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_google_search_option": "Google Search cannot be enabled alongside any Assist capability, this can only be used when Assist is set to \"No control\"."
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
|
@ -16,13 +16,13 @@
|
||||
"name": "Panel light"
|
||||
},
|
||||
"quiet": {
|
||||
"name": "Quiet"
|
||||
"name": "Quiet mode"
|
||||
},
|
||||
"fresh_air": {
|
||||
"name": "Fresh air"
|
||||
},
|
||||
"xfan": {
|
||||
"name": "XFan"
|
||||
"name": "Xtra fan"
|
||||
},
|
||||
"health_mode": {
|
||||
"name": "Health mode"
|
||||
|
@ -265,6 +265,11 @@
|
||||
"version_latest": {
|
||||
"name": "Newest version"
|
||||
}
|
||||
},
|
||||
"update": {
|
||||
"update": {
|
||||
"name": "[%key:component::update::title%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
|
@ -39,7 +39,7 @@ from .entity import (
|
||||
from .update_helper import update_addon, update_core
|
||||
|
||||
ENTITY_DESCRIPTION = UpdateEntityDescription(
|
||||
name="Update",
|
||||
translation_key="update",
|
||||
key=ATTR_VERSION_LATEST,
|
||||
)
|
||||
|
||||
|
@ -8,7 +8,7 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"country": "Country"
|
||||
"country": "[%key:common::config_flow::data::country%]"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
|
@ -244,6 +244,7 @@ class HomeConnectDoorBinarySensor(HomeConnectBinarySensor):
|
||||
BSH_DOOR_STATE_LOCKED: False,
|
||||
BSH_DOOR_STATE_OPEN: True,
|
||||
},
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
)
|
||||
self._attr_unique_id = f"{appliance.info.ha_id}-Door"
|
||||
@ -283,7 +284,8 @@ class HomeConnectDoorBinarySensor(HomeConnectBinarySensor):
|
||||
DOMAIN,
|
||||
f"deprecated_binary_common_door_sensor_{self.entity_id}",
|
||||
breaks_in_ha_version="2025.5.0",
|
||||
is_fixable=False,
|
||||
is_fixable=True,
|
||||
is_persistent=True,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_binary_common_door_sensor",
|
||||
translation_placeholders={
|
||||
|
@ -73,6 +73,19 @@ class HomeConnectApplianceData:
|
||||
self.settings.update(other.settings)
|
||||
self.status.update(other.status)
|
||||
|
||||
@classmethod
|
||||
def empty(cls, appliance: HomeAppliance) -> HomeConnectApplianceData:
|
||||
"""Return empty data."""
|
||||
return cls(
|
||||
commands=set(),
|
||||
events={},
|
||||
info=appliance,
|
||||
options={},
|
||||
programs=[],
|
||||
settings={},
|
||||
status={},
|
||||
)
|
||||
|
||||
|
||||
class HomeConnectCoordinator(
|
||||
DataUpdateCoordinator[dict[str, HomeConnectApplianceData]]
|
||||
@ -358,15 +371,7 @@ class HomeConnectCoordinator(
|
||||
model=appliance.vib,
|
||||
)
|
||||
if appliance.ha_id not in self.data:
|
||||
self.data[appliance.ha_id] = HomeConnectApplianceData(
|
||||
commands=set(),
|
||||
events={},
|
||||
info=appliance,
|
||||
options={},
|
||||
programs=[],
|
||||
settings={},
|
||||
status={},
|
||||
)
|
||||
self.data[appliance.ha_id] = HomeConnectApplianceData.empty(appliance)
|
||||
else:
|
||||
self.data[appliance.ha_id].info.connected = appliance.connected
|
||||
old_appliances.remove(appliance.ha_id)
|
||||
@ -402,6 +407,15 @@ class HomeConnectCoordinator(
|
||||
name=appliance.name,
|
||||
model=appliance.vib,
|
||||
)
|
||||
if not appliance.connected:
|
||||
_LOGGER.debug(
|
||||
"Appliance %s is not connected, skipping data fetch",
|
||||
appliance.ha_id,
|
||||
)
|
||||
if appliance_data_to_update:
|
||||
appliance_data_to_update.info.connected = False
|
||||
return appliance_data_to_update
|
||||
return HomeConnectApplianceData.empty(appliance)
|
||||
try:
|
||||
settings = {
|
||||
setting.key: setting
|
||||
|
@ -64,7 +64,6 @@ set_program_and_options:
|
||||
- selected_program
|
||||
program:
|
||||
example: dishcare_dishwasher_program_auto2
|
||||
required: true
|
||||
selector:
|
||||
select:
|
||||
mode: dropdown
|
||||
|
@ -134,15 +134,47 @@
|
||||
},
|
||||
"deprecated_binary_common_door_sensor": {
|
||||
"title": "Deprecated binary door sensor detected in some automations or scripts",
|
||||
"description": "The binary door sensor `{entity}`, which is deprecated, is used in the following automations or scripts:\n{items}\n\nA sensor entity with additional possible states is available and should be used going forward; Please use it on the above automations or scripts to fix this issue."
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"title": "[%key:component::home_connect::issues::deprecated_binary_common_door_sensor::title%]",
|
||||
"description": "The binary door sensor `{entity}`, which is deprecated, is used in the following automations or scripts:\n{items}\n\nA sensor entity with additional possible states is available and should be used going forward; Please use it on the above automations or scripts to fix this issue."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"deprecated_command_actions": {
|
||||
"title": "The command related actions are deprecated in favor of the new buttons",
|
||||
"description": "The `pause_program` and `resume_program` actions have been deprecated in favor of new button entities, if the command is available for your appliance. Please update your automations, scripts and panels that use this action to use the button entities instead, and press on submit to fix the issue."
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"title": "[%key:component::home_connect::issues::deprecated_command_actions::title%]",
|
||||
"description": "The `pause_program` and `resume_program` actions have been deprecated in favor of new button entities, if the command is available for your appliance. Please update your automations, scripts and panels that use this action to use the button entities instead, and press on submit to fix the issue."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"deprecated_program_switch_in_automations_scripts": {
|
||||
"title": "Deprecated program switch detected in some automations or scripts",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"title": "[%key:component::home_connect::issues::deprecated_program_switch_in_automations_scripts::title%]",
|
||||
"description": "Program switches are deprecated and {entity_id} is used in the following automations or scripts:\n{items}\n\nYou can use the active program select entity to run the program without any additional options and get the current running program on the above automations or scripts to fix this issue."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"deprecated_program_switch": {
|
||||
"title": "Deprecated program switch detected in some automations or scripts",
|
||||
"description": "Program switches are deprecated and {entity_id} is used in the following automations or scripts:\n{items}\n\nYou can use the active program select entity to run the program without any additional options and get the current running program on the above automations or scripts to fix this issue."
|
||||
"title": "Deprecated program switch entities",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"title": "[%key:component::home_connect::issues::deprecated_program_switch::title%]",
|
||||
"description": "The switch entity `{entity_id}` and all the other program switches are deprecated.\n\nPlease use the active program select entity instead."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"deprecated_set_program_and_option_actions": {
|
||||
"title": "The executed action is deprecated",
|
||||
|
@ -266,7 +266,10 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity):
|
||||
super().__init__(
|
||||
coordinator,
|
||||
appliance,
|
||||
SwitchEntityDescription(key=EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM),
|
||||
SwitchEntityDescription(
|
||||
key=EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
)
|
||||
self._attr_name = f"{appliance.info.name} {desc}"
|
||||
self._attr_unique_id = f"{appliance.info.ha_id}-{desc}"
|
||||
@ -304,11 +307,12 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity):
|
||||
async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
f"deprecated_program_switch_{self.entity_id}",
|
||||
f"deprecated_program_switch_in_automations_scripts_{self.entity_id}",
|
||||
breaks_in_ha_version="2025.6.0",
|
||||
is_fixable=False,
|
||||
is_fixable=True,
|
||||
is_persistent=True,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_program_switch",
|
||||
translation_key="deprecated_program_switch_in_automations_scripts",
|
||||
translation_placeholders={
|
||||
"entity_id": self.entity_id,
|
||||
"items": "\n".join(items_list),
|
||||
@ -317,12 +321,34 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity):
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Call when entity will be removed from hass."""
|
||||
async_delete_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
f"deprecated_program_switch_in_automations_scripts_{self.entity_id}",
|
||||
)
|
||||
async_delete_issue(
|
||||
self.hass, DOMAIN, f"deprecated_program_switch_{self.entity_id}"
|
||||
)
|
||||
|
||||
def create_action_handler_issue(self) -> None:
|
||||
"""Create deprecation issue."""
|
||||
async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
f"deprecated_program_switch_{self.entity_id}",
|
||||
breaks_in_ha_version="2025.6.0",
|
||||
is_fixable=True,
|
||||
is_persistent=True,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_program_switch",
|
||||
translation_placeholders={
|
||||
"entity_id": self.entity_id,
|
||||
},
|
||||
)
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Start the program."""
|
||||
self.create_action_handler_issue()
|
||||
try:
|
||||
await self.coordinator.client.start_program(
|
||||
self.appliance.info.ha_id, program_key=self.program.key
|
||||
@ -339,6 +365,7 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity):
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Stop the program."""
|
||||
self.create_action_handler_issue()
|
||||
try:
|
||||
await self.coordinator.client.stop_program(self.appliance.info.ha_id)
|
||||
except HomeConnectError as err:
|
||||
|
@ -31,7 +31,6 @@ class FirmwareUpdateCoordinator(DataUpdateCoordinator[FirmwareManifest]):
|
||||
_LOGGER,
|
||||
name="firmware update coordinator",
|
||||
update_interval=FIRMWARE_REFRESH_INTERVAL,
|
||||
always_update=False,
|
||||
)
|
||||
self.hass = hass
|
||||
self.session = session
|
||||
|
@ -33,6 +33,7 @@ from .util import (
|
||||
OwningIntegration,
|
||||
get_otbr_addon_manager,
|
||||
get_zigbee_flasher_addon_manager,
|
||||
guess_firmware_info,
|
||||
guess_hardware_owners,
|
||||
probe_silabs_firmware_info,
|
||||
)
|
||||
@ -511,6 +512,16 @@ class BaseFirmwareConfigFlow(BaseFirmwareInstallFlow, ConfigFlow):
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Confirm a discovery."""
|
||||
assert self._device is not None
|
||||
fw_info = await guess_firmware_info(self.hass, self._device)
|
||||
|
||||
# If our guess for the firmware type is actually running, we can save the user
|
||||
# an unnecessary confirmation and silently confirm the flow
|
||||
for owner in fw_info.owners:
|
||||
if await owner.is_running(self.hass):
|
||||
self._probed_firmware_info = fw_info
|
||||
return self._async_flow_finished()
|
||||
|
||||
return await self.async_step_pick_firmware()
|
||||
|
||||
|
||||
|
@ -95,8 +95,7 @@ class BaseFirmwareUpdateEntity(
|
||||
_attr_supported_features = (
|
||||
UpdateEntityFeature.INSTALL | UpdateEntityFeature.PROGRESS
|
||||
)
|
||||
# Until this entity can be associated with a device, we must manually name it
|
||||
_attr_has_entity_name = False
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@ -195,11 +194,7 @@ class BaseFirmwareUpdateEntity(
|
||||
|
||||
def _update_attributes(self) -> None:
|
||||
"""Recompute the attributes of the entity."""
|
||||
|
||||
# This entity is not currently associated with a device so we must manually
|
||||
# give it a name
|
||||
self._attr_name = f"{self._config_entry.title} Update"
|
||||
self._attr_title = self.entity_description.firmware_name or "unknown"
|
||||
self._attr_title = self.entity_description.firmware_name or "Unknown"
|
||||
|
||||
if (
|
||||
self._current_firmware_info is None
|
||||
|
@ -3,19 +3,79 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os.path
|
||||
|
||||
from homeassistant.components.homeassistant_hardware.util import guess_firmware_info
|
||||
from homeassistant.components.usb import (
|
||||
USBDevice,
|
||||
async_register_port_event_callback,
|
||||
scan_serial_ports,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DESCRIPTION, DEVICE, FIRMWARE, FIRMWARE_VERSION, PRODUCT
|
||||
from .const import (
|
||||
DESCRIPTION,
|
||||
DEVICE,
|
||||
DOMAIN,
|
||||
FIRMWARE,
|
||||
FIRMWARE_VERSION,
|
||||
MANUFACTURER,
|
||||
PID,
|
||||
PRODUCT,
|
||||
SERIAL_NUMBER,
|
||||
VID,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the ZBT-1 integration."""
|
||||
|
||||
@callback
|
||||
def async_port_event_callback(
|
||||
added: set[USBDevice], removed: set[USBDevice]
|
||||
) -> None:
|
||||
"""Handle USB port events."""
|
||||
current_entries_by_path = {
|
||||
entry.data[DEVICE]: entry
|
||||
for entry in hass.config_entries.async_entries(DOMAIN)
|
||||
}
|
||||
|
||||
for device in added | removed:
|
||||
path = device.device
|
||||
entry = current_entries_by_path.get(path)
|
||||
|
||||
if entry is not None:
|
||||
_LOGGER.debug(
|
||||
"Device %r has changed state, reloading config entry %s",
|
||||
path,
|
||||
entry,
|
||||
)
|
||||
hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
|
||||
async_register_port_event_callback(hass, async_port_event_callback)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up a Home Assistant SkyConnect config entry."""
|
||||
|
||||
# Postpone loading the config entry if the device is missing
|
||||
device_path = entry.data[DEVICE]
|
||||
if not await hass.async_add_executor_job(os.path.exists, device_path):
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="device_disconnected",
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, ["update"])
|
||||
|
||||
return True
|
||||
@ -23,6 +83,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
await hass.config_entries.async_unload_platforms(entry, ["update"])
|
||||
return True
|
||||
|
||||
|
||||
@ -30,7 +91,7 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
"""Migrate old entry."""
|
||||
|
||||
_LOGGER.debug(
|
||||
"Migrating from version %s:%s", config_entry.version, config_entry.minor_version
|
||||
"Migrating from version %s.%s", config_entry.version, config_entry.minor_version
|
||||
)
|
||||
|
||||
if config_entry.version == 1:
|
||||
@ -65,6 +126,43 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
minor_version=3,
|
||||
)
|
||||
|
||||
if config_entry.minor_version == 3:
|
||||
# Old SkyConnect config entries were missing keys
|
||||
if any(
|
||||
key not in config_entry.data
|
||||
for key in (VID, PID, MANUFACTURER, PRODUCT, SERIAL_NUMBER)
|
||||
):
|
||||
serial_ports = await hass.async_add_executor_job(scan_serial_ports)
|
||||
serial_ports_info = {port.device: port for port in serial_ports}
|
||||
device = config_entry.data[DEVICE]
|
||||
|
||||
if not (usb_info := serial_ports_info.get(device)):
|
||||
raise HomeAssistantError(
|
||||
f"USB device {device} is missing, cannot migrate"
|
||||
)
|
||||
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry,
|
||||
data={
|
||||
**config_entry.data,
|
||||
VID: usb_info.vid,
|
||||
PID: usb_info.pid,
|
||||
MANUFACTURER: usb_info.manufacturer,
|
||||
PRODUCT: usb_info.description,
|
||||
DESCRIPTION: usb_info.description,
|
||||
SERIAL_NUMBER: usb_info.serial_number,
|
||||
},
|
||||
version=1,
|
||||
minor_version=4,
|
||||
)
|
||||
else:
|
||||
# Existing entries are migrated by just incrementing the version
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry,
|
||||
version=1,
|
||||
minor_version=4,
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
"Migration to version %s.%s successful",
|
||||
config_entry.version,
|
||||
|
@ -81,7 +81,7 @@ class HomeAssistantSkyConnectConfigFlow(
|
||||
"""Handle a config flow for Home Assistant SkyConnect."""
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 3
|
||||
MINOR_VERSION = 4
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""Initialize the config flow."""
|
||||
|
@ -5,17 +5,21 @@ from __future__ import annotations
|
||||
from homeassistant.components.hardware.models import HardwareInfo, USBInfo
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from .config_flow import HomeAssistantSkyConnectConfigFlow
|
||||
from .const import DOMAIN
|
||||
from .util import get_hardware_variant
|
||||
|
||||
DOCUMENTATION_URL = "https://skyconnect.home-assistant.io/documentation/"
|
||||
EXPECTED_ENTRY_VERSION = (
|
||||
HomeAssistantSkyConnectConfigFlow.VERSION,
|
||||
HomeAssistantSkyConnectConfigFlow.MINOR_VERSION,
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_info(hass: HomeAssistant) -> list[HardwareInfo]:
|
||||
"""Return board info."""
|
||||
entries = hass.config_entries.async_entries(DOMAIN)
|
||||
|
||||
return [
|
||||
HardwareInfo(
|
||||
board=None,
|
||||
@ -31,4 +35,6 @@ def async_info(hass: HomeAssistant) -> list[HardwareInfo]:
|
||||
url=DOCUMENTATION_URL,
|
||||
)
|
||||
for entry in entries
|
||||
# Ignore unmigrated config entries in the hardware page
|
||||
if (entry.version, entry.minor_version) == EXPECTED_ENTRY_VERSION
|
||||
]
|
||||
|
@ -195,5 +195,10 @@
|
||||
"run_zigbee_flasher_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::run_zigbee_flasher_addon%]",
|
||||
"uninstall_zigbee_flasher_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::uninstall_zigbee_flasher_addon%]"
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"device_disconnected": {
|
||||
"message": "The device is not plugged in"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,11 +21,20 @@ from homeassistant.components.update import UpdateDeviceClass
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import FIRMWARE, FIRMWARE_VERSION, NABU_CASA_FIRMWARE_RELEASES_URL
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
FIRMWARE,
|
||||
FIRMWARE_VERSION,
|
||||
NABU_CASA_FIRMWARE_RELEASES_URL,
|
||||
PRODUCT,
|
||||
SERIAL_NUMBER,
|
||||
HardwareVariant,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -42,7 +51,7 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[
|
||||
fw_type="skyconnect_zigbee_ncp",
|
||||
version_key="ezsp_version",
|
||||
expected_firmware_type=ApplicationType.EZSP,
|
||||
firmware_name="EmberZNet",
|
||||
firmware_name="EmberZNet Zigbee",
|
||||
),
|
||||
ApplicationType.SPINEL: FirmwareUpdateEntityDescription(
|
||||
key="firmware",
|
||||
@ -55,6 +64,28 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[
|
||||
expected_firmware_type=ApplicationType.SPINEL,
|
||||
firmware_name="OpenThread RCP",
|
||||
),
|
||||
ApplicationType.CPC: FirmwareUpdateEntityDescription(
|
||||
key="firmware",
|
||||
display_precision=0,
|
||||
device_class=UpdateDeviceClass.FIRMWARE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
version_parser=lambda fw: fw,
|
||||
fw_type="skyconnect_multipan",
|
||||
version_key="cpc_version",
|
||||
expected_firmware_type=ApplicationType.CPC,
|
||||
firmware_name="Multiprotocol",
|
||||
),
|
||||
ApplicationType.GECKO_BOOTLOADER: FirmwareUpdateEntityDescription(
|
||||
key="firmware",
|
||||
display_precision=0,
|
||||
device_class=UpdateDeviceClass.FIRMWARE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
version_parser=lambda fw: fw,
|
||||
fw_type=None, # We don't want to update the bootloader
|
||||
version_key="gecko_bootloader_version",
|
||||
expected_firmware_type=ApplicationType.GECKO_BOOTLOADER,
|
||||
firmware_name="Gecko Bootloader",
|
||||
),
|
||||
None: FirmwareUpdateEntityDescription(
|
||||
key="firmware",
|
||||
display_precision=0,
|
||||
@ -77,9 +108,16 @@ def _async_create_update_entity(
|
||||
) -> FirmwareUpdateEntity:
|
||||
"""Create an update entity that handles firmware type changes."""
|
||||
firmware_type = config_entry.data[FIRMWARE]
|
||||
entity_description = FIRMWARE_ENTITY_DESCRIPTIONS[
|
||||
ApplicationType(firmware_type) if firmware_type is not None else None
|
||||
]
|
||||
|
||||
try:
|
||||
entity_description = FIRMWARE_ENTITY_DESCRIPTIONS[
|
||||
ApplicationType(firmware_type)
|
||||
]
|
||||
except (KeyError, ValueError):
|
||||
_LOGGER.debug(
|
||||
"Unknown firmware type %r, using default entity description", firmware_type
|
||||
)
|
||||
entity_description = FIRMWARE_ENTITY_DESCRIPTIONS[None]
|
||||
|
||||
entity = FirmwareUpdateEntity(
|
||||
device=config_entry.data["device"],
|
||||
@ -141,8 +179,18 @@ class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
|
||||
"""Initialize the SkyConnect firmware update entity."""
|
||||
super().__init__(device, config_entry, update_coordinator, entity_description)
|
||||
|
||||
self._attr_unique_id = (
|
||||
f"{self._config_entry.data['serial_number']}_{self.entity_description.key}"
|
||||
variant = HardwareVariant.from_usb_product_name(
|
||||
self._config_entry.data[PRODUCT]
|
||||
)
|
||||
serial_number = self._config_entry.data[SERIAL_NUMBER]
|
||||
|
||||
self._attr_unique_id = f"{serial_number}_{self.entity_description.key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, serial_number)},
|
||||
name=f"{variant.full_name} ({serial_number[:8]})",
|
||||
model=variant.full_name,
|
||||
manufacturer="Nabu Casa",
|
||||
serial_number=serial_number,
|
||||
)
|
||||
|
||||
# Use the cached firmware info if it exists
|
||||
@ -155,6 +203,17 @@ class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
|
||||
source="homeassistant_sky_connect",
|
||||
)
|
||||
|
||||
def _update_attributes(self) -> None:
|
||||
"""Recompute the attributes of the entity."""
|
||||
super()._update_attributes()
|
||||
|
||||
assert self.device_entry is not None
|
||||
device_registry = dr.async_get(self.hass)
|
||||
device_registry.async_update_device(
|
||||
device_id=self.device_entry.id,
|
||||
sw_version=f"{self.entity_description.firmware_name} {self._attr_installed_version}",
|
||||
)
|
||||
|
||||
@callback
|
||||
def _firmware_info_callback(self, firmware_info: FirmwareInfo) -> None:
|
||||
"""Handle updated firmware info being pushed by an integration."""
|
||||
|
@ -62,6 +62,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
await hass.config_entries.async_unload_platforms(entry, ["update"])
|
||||
return True
|
||||
|
||||
|
||||
|
@ -2,8 +2,9 @@
|
||||
|
||||
DOMAIN = "homeassistant_yellow"
|
||||
|
||||
RADIO_MODEL = "Home Assistant Yellow"
|
||||
RADIO_MANUFACTURER = "Nabu Casa"
|
||||
MODEL = "Home Assistant Yellow"
|
||||
MANUFACTURER = "Nabu Casa"
|
||||
|
||||
RADIO_DEVICE = "/dev/ttyAMA1"
|
||||
|
||||
ZHA_HW_DISCOVERY_DATA = {
|
||||
|
@ -149,5 +149,12 @@
|
||||
"run_zigbee_flasher_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::run_zigbee_flasher_addon%]",
|
||||
"uninstall_zigbee_flasher_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::uninstall_zigbee_flasher_addon%]"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"update": {
|
||||
"radio_firmware": {
|
||||
"name": "Radio firmware"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,13 +21,17 @@ from homeassistant.components.update import UpdateDeviceClass
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
FIRMWARE,
|
||||
FIRMWARE_VERSION,
|
||||
MANUFACTURER,
|
||||
MODEL,
|
||||
NABU_CASA_FIRMWARE_RELEASES_URL,
|
||||
RADIO_DEVICE,
|
||||
)
|
||||
@ -39,7 +43,8 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[
|
||||
ApplicationType | None, FirmwareUpdateEntityDescription
|
||||
] = {
|
||||
ApplicationType.EZSP: FirmwareUpdateEntityDescription(
|
||||
key="firmware",
|
||||
key="radio_firmware",
|
||||
translation_key="radio_firmware",
|
||||
display_precision=0,
|
||||
device_class=UpdateDeviceClass.FIRMWARE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
@ -47,10 +52,11 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[
|
||||
fw_type="yellow_zigbee_ncp",
|
||||
version_key="ezsp_version",
|
||||
expected_firmware_type=ApplicationType.EZSP,
|
||||
firmware_name="EmberZNet",
|
||||
firmware_name="EmberZNet Zigbee",
|
||||
),
|
||||
ApplicationType.SPINEL: FirmwareUpdateEntityDescription(
|
||||
key="firmware",
|
||||
key="radio_firmware",
|
||||
translation_key="radio_firmware",
|
||||
display_precision=0,
|
||||
device_class=UpdateDeviceClass.FIRMWARE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
@ -60,8 +66,33 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[
|
||||
expected_firmware_type=ApplicationType.SPINEL,
|
||||
firmware_name="OpenThread RCP",
|
||||
),
|
||||
ApplicationType.CPC: FirmwareUpdateEntityDescription(
|
||||
key="radio_firmware",
|
||||
translation_key="radio_firmware",
|
||||
display_precision=0,
|
||||
device_class=UpdateDeviceClass.FIRMWARE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
version_parser=lambda fw: fw,
|
||||
fw_type="yellow_multipan",
|
||||
version_key="cpc_version",
|
||||
expected_firmware_type=ApplicationType.CPC,
|
||||
firmware_name="Multiprotocol",
|
||||
),
|
||||
ApplicationType.GECKO_BOOTLOADER: FirmwareUpdateEntityDescription(
|
||||
key="radio_firmware",
|
||||
translation_key="radio_firmware",
|
||||
display_precision=0,
|
||||
device_class=UpdateDeviceClass.FIRMWARE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
version_parser=lambda fw: fw,
|
||||
fw_type=None, # We don't want to update the bootloader
|
||||
version_key="gecko_bootloader_version",
|
||||
expected_firmware_type=ApplicationType.GECKO_BOOTLOADER,
|
||||
firmware_name="Gecko Bootloader",
|
||||
),
|
||||
None: FirmwareUpdateEntityDescription(
|
||||
key="firmware",
|
||||
key="radio_firmware",
|
||||
translation_key="radio_firmware",
|
||||
display_precision=0,
|
||||
device_class=UpdateDeviceClass.FIRMWARE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
@ -82,9 +113,16 @@ def _async_create_update_entity(
|
||||
) -> FirmwareUpdateEntity:
|
||||
"""Create an update entity that handles firmware type changes."""
|
||||
firmware_type = config_entry.data[FIRMWARE]
|
||||
entity_description = FIRMWARE_ENTITY_DESCRIPTIONS[
|
||||
ApplicationType(firmware_type) if firmware_type is not None else None
|
||||
]
|
||||
|
||||
try:
|
||||
entity_description = FIRMWARE_ENTITY_DESCRIPTIONS[
|
||||
ApplicationType(firmware_type)
|
||||
]
|
||||
except (KeyError, ValueError):
|
||||
_LOGGER.debug(
|
||||
"Unknown firmware type %r, using default entity description", firmware_type
|
||||
)
|
||||
entity_description = FIRMWARE_ENTITY_DESCRIPTIONS[None]
|
||||
|
||||
entity = FirmwareUpdateEntity(
|
||||
device=RADIO_DEVICE,
|
||||
@ -145,8 +183,13 @@ class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
|
||||
) -> None:
|
||||
"""Initialize the Yellow firmware update entity."""
|
||||
super().__init__(device, config_entry, update_coordinator, entity_description)
|
||||
|
||||
self._attr_unique_id = self.entity_description.key
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, "yellow")},
|
||||
name=MODEL,
|
||||
model=MODEL,
|
||||
manufacturer=MANUFACTURER,
|
||||
)
|
||||
|
||||
# Use the cached firmware info if it exists
|
||||
if self._config_entry.data[FIRMWARE] is not None:
|
||||
@ -158,6 +201,17 @@ class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
|
||||
source="homeassistant_yellow",
|
||||
)
|
||||
|
||||
def _update_attributes(self) -> None:
|
||||
"""Recompute the attributes of the entity."""
|
||||
super()._update_attributes()
|
||||
|
||||
assert self.device_entry is not None
|
||||
device_registry = dr.async_get(self.hass)
|
||||
device_registry.async_update_device(
|
||||
device_id=self.device_entry.id,
|
||||
sw_version=f"{self.entity_description.firmware_name} {self._attr_installed_version}",
|
||||
)
|
||||
|
||||
@callback
|
||||
def _firmware_info_callback(self, firmware_info: FirmwareInfo) -> None:
|
||||
"""Handle updated firmware info being pushed by an integration."""
|
||||
|
@ -14,6 +14,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiohomekit", "commentjson"],
|
||||
"requirements": ["aiohomekit==3.2.8"],
|
||||
"requirements": ["aiohomekit==3.2.13"],
|
||||
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."]
|
||||
}
|
||||
|
@ -1,4 +1,28 @@
|
||||
{
|
||||
"entity": {
|
||||
"light": {
|
||||
"hue_light": {
|
||||
"state_attributes": {
|
||||
"effect": {
|
||||
"state": {
|
||||
"candle": "mdi:candle",
|
||||
"sparkle": "mdi:shimmer",
|
||||
"glisten": "mdi:creation",
|
||||
"sunrise": "mdi:weather-sunset-up",
|
||||
"sunset": "mdi:weather-sunset",
|
||||
"fire": "mdi:fire",
|
||||
"prism": "mdi:triangle-outline",
|
||||
"opal": "mdi:diamond-stone",
|
||||
"underwater": "mdi:waves",
|
||||
"cosmos": "mdi:star-shooting",
|
||||
"sunbeam": "mdi:spotlight-beam",
|
||||
"enchant": "mdi:magic-staff"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"hue_activate_scene": {
|
||||
"service": "mdi:palette"
|
||||
|
@ -197,5 +197,11 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_effect_none": {
|
||||
"title": "Light turned on with deprecated effect",
|
||||
"description": "A light was turned on with the deprecated effect `None`. This has been replaced with `off`. Please update any automations, scenes, or scripts that use this effect."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ from homeassistant.components.light import (
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.util import color as color_util
|
||||
|
||||
from ..bridge import HueBridge
|
||||
@ -44,6 +45,9 @@ FALLBACK_MIN_KELVIN = 6500
|
||||
FALLBACK_MAX_KELVIN = 2000
|
||||
FALLBACK_KELVIN = 5800 # halfway
|
||||
|
||||
# HA 2025.4 replaced the deprecated effect "None" with HA default "off"
|
||||
DEPRECATED_EFFECT_NONE = "None"
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@ -233,6 +237,23 @@ class HueLight(HueBaseEntity, LightEntity):
|
||||
self._color_temp_active = color_temp is not None
|
||||
flash = kwargs.get(ATTR_FLASH)
|
||||
effect = effect_str = kwargs.get(ATTR_EFFECT)
|
||||
if effect_str == DEPRECATED_EFFECT_NONE:
|
||||
# deprecated effect "None" is now "off"
|
||||
effect_str = EFFECT_OFF
|
||||
async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
"deprecated_effect_none",
|
||||
breaks_in_ha_version="2025.10.0",
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_effect_none",
|
||||
)
|
||||
self.logger.warning(
|
||||
"Detected deprecated effect 'None' in %s, use 'off' instead. "
|
||||
"This will stop working in HA 2025.10",
|
||||
self.entity_id,
|
||||
)
|
||||
if effect_str == EFFECT_OFF:
|
||||
# ignore effect if set to "off" and we have no effect active
|
||||
# the special effect "off" is only used to stop an active effect
|
||||
|
@ -227,12 +227,16 @@ def _get_work_area_names(data: MowerAttributes) -> list[str]:
|
||||
@callback
|
||||
def _get_current_work_area_name(data: MowerAttributes) -> str:
|
||||
"""Return the name of the current work area."""
|
||||
if data.mower.work_area_id is None:
|
||||
return STATE_NO_WORK_AREA_ACTIVE
|
||||
if TYPE_CHECKING:
|
||||
# Sensor does not get created if values are None
|
||||
assert data.work_areas is not None
|
||||
return data.work_areas[data.mower.work_area_id].name
|
||||
if (
|
||||
data.mower.work_area_id is not None
|
||||
and data.mower.work_area_id in data.work_areas
|
||||
):
|
||||
return data.work_areas[data.mower.work_area_id].name
|
||||
|
||||
return STATE_NO_WORK_AREA_ACTIVE
|
||||
|
||||
|
||||
@callback
|
||||
|
@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/iaqualink",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["iaqualink"],
|
||||
"requirements": ["iaqualink==0.5.0", "h2==4.1.0"],
|
||||
"requirements": ["iaqualink==0.5.3", "h2==4.1.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
@ -10,8 +10,8 @@
|
||||
},
|
||||
"data_description": {
|
||||
"host": "Hostname or IP-address of the Intergas gateway.",
|
||||
"username": "The username to log into the gateway. This is `admin` in most cases.",
|
||||
"password": "The password to log into the gateway, is printed at the bottom of the gateway or is `intergas` for some older devices."
|
||||
"username": "The username to log in to the gateway. This is `admin` in most cases.",
|
||||
"password": "The password to log in to the gateway, is printed at the bottom of the gateway or is `intergas` for some older devices."
|
||||
}
|
||||
},
|
||||
"dhcp_auth": {
|
||||
@ -22,8 +22,8 @@
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"username": "The username to log into the gateway. This is `admin` in most cases.",
|
||||
"password": "The password to log into the gateway, is printed at the bottom of the Gateway or is `intergas` for some older devices."
|
||||
"username": "[%key:component::incomfort::config::step::user::data_description::username%]",
|
||||
"password": "[%key:component::incomfort::config::step::user::data_description::password%]"
|
||||
}
|
||||
},
|
||||
"dhcp_confirm": {
|
||||
|
@ -138,7 +138,7 @@ async def async_setup_entry(
|
||||
for vtype, _, vid in isy.variables.children:
|
||||
numbers.append(isy.variables[vtype][vid])
|
||||
if (
|
||||
isy.conf[CONFIG_NETWORKING] or isy.conf[CONFIG_PORTAL]
|
||||
isy.conf[CONFIG_NETWORKING] or isy.conf.get(CONFIG_PORTAL)
|
||||
) and isy.networking.nobjs:
|
||||
isy_data.devices[CONF_NETWORK] = _create_service_device_info(
|
||||
isy, name=CONFIG_NETWORKING, unique_id=CONF_NETWORK
|
||||
|
@ -24,7 +24,7 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyisy"],
|
||||
"requirements": ["pyisy==3.1.14"],
|
||||
"requirements": ["pyisy==3.1.15"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Universal Devices Inc.",
|
||||
|
@ -6,6 +6,7 @@ count_omer:
|
||||
selector:
|
||||
date:
|
||||
nusach:
|
||||
required: true
|
||||
example: "sfarad"
|
||||
default: "sfarad"
|
||||
selector:
|
||||
|
@ -2,19 +2,19 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"import_confirm": {
|
||||
"title": "Import Konnected Device",
|
||||
"description": "A Konnected Alarm Panel with ID {id} has been discovered in configuration.yaml. This flow will allow you to import it into a config entry."
|
||||
"title": "Import Konnected device",
|
||||
"description": "A Konnected alarm panel with ID {id} has been discovered in configuration.yaml. This flow will allow you to import it into a config entry."
|
||||
},
|
||||
"user": {
|
||||
"description": "Please enter the host information for your Konnected Panel.",
|
||||
"description": "Please enter the host information for your Konnected panel.",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::ip%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
}
|
||||
},
|
||||
"confirm": {
|
||||
"title": "Konnected Device Ready",
|
||||
"description": "Model: {model}\nID: {id}\nHost: {host}\nPort: {port}\n\nYou can configure the IO and panel behavior in the Konnected Alarm Panel settings."
|
||||
"title": "Konnected device ready",
|
||||
"description": "Model: {model}\nID: {id}\nHost: {host}\nPort: {port}\n\nYou can configure the IO and panel behavior in the Konnected alarm panel settings."
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@ -45,8 +45,8 @@
|
||||
}
|
||||
},
|
||||
"options_io_ext": {
|
||||
"title": "Configure Extended I/O",
|
||||
"description": "Select the configuration of the remaining I/O below. You'll be able to configure detailed options in the next steps.",
|
||||
"title": "Configure extended I/O",
|
||||
"description": "Select the configuration of the remaining I/O below. You'll be able to configure detailed options in the next steps.",
|
||||
"data": {
|
||||
"8": "Zone 8",
|
||||
"9": "Zone 9",
|
||||
@ -59,25 +59,25 @@
|
||||
}
|
||||
},
|
||||
"options_binary": {
|
||||
"title": "Configure Binary Sensor",
|
||||
"title": "Configure binary sensor",
|
||||
"description": "{zone} options",
|
||||
"data": {
|
||||
"type": "Binary Sensor Type",
|
||||
"type": "Binary sensor type",
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
"inverse": "Invert the open/close state"
|
||||
}
|
||||
},
|
||||
"options_digital": {
|
||||
"title": "Configure Digital Sensor",
|
||||
"title": "Configure digital sensor",
|
||||
"description": "[%key:component::konnected::options::step::options_binary::description%]",
|
||||
"data": {
|
||||
"type": "Sensor Type",
|
||||
"type": "Sensor type",
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
"poll_interval": "Poll Interval (minutes)"
|
||||
"poll_interval": "Poll interval (minutes)"
|
||||
}
|
||||
},
|
||||
"options_switch": {
|
||||
"title": "Configure Switchable Output",
|
||||
"title": "Configure switchable output",
|
||||
"description": "{zone} options: state {state}",
|
||||
"data": {
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
@ -89,18 +89,18 @@
|
||||
}
|
||||
},
|
||||
"options_misc": {
|
||||
"title": "Configure Misc",
|
||||
"title": "Configure misc",
|
||||
"description": "Please select the desired behavior for your panel",
|
||||
"data": {
|
||||
"discovery": "Respond to discovery requests on your network",
|
||||
"blink": "Blink panel LED on when sending state change",
|
||||
"override_api_host": "Override default Home Assistant API host panel URL",
|
||||
"api_host": "Override API host URL"
|
||||
"override_api_host": "Override default Home Assistant API host URL",
|
||||
"api_host": "Custom API host URL"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"bad_host": "Invalid Override API host URL"
|
||||
"bad_host": "Invalid custom API host URL"
|
||||
},
|
||||
"abort": {
|
||||
"not_konn_panel": "[%key:component::konnected::config::abort::not_konn_panel%]"
|
||||
|
@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ld2410_ble",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["bluetooth-data-tools==1.26.1", "ld2410-ble==0.1.1"]
|
||||
"requirements": ["bluetooth-data-tools==1.26.5", "ld2410-ble==0.1.1"]
|
||||
}
|
||||
|
@ -35,5 +35,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/led_ble",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["bluetooth-data-tools==1.26.1", "led-ble==1.1.6"]
|
||||
"requirements": ["bluetooth-data-tools==1.26.5", "led-ble==1.1.6"]
|
||||
}
|
||||
|
@ -63,10 +63,12 @@ class DeviceDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
|
||||
# Add a callback to handle core config update.
|
||||
self.unit_system: str | None = None
|
||||
self.hass.bus.async_listen(
|
||||
event_type=EVENT_CORE_CONFIG_UPDATE,
|
||||
listener=self._handle_update_config,
|
||||
event_filter=self.async_config_update_filter,
|
||||
self.config_entry.async_on_unload(
|
||||
self.hass.bus.async_listen(
|
||||
event_type=EVENT_CORE_CONFIG_UPDATE,
|
||||
listener=self._handle_update_config,
|
||||
event_filter=self.async_config_update_filter,
|
||||
)
|
||||
)
|
||||
|
||||
async def _handle_update_config(self, _: Event) -> None:
|
||||
|
@ -1,7 +1,15 @@
|
||||
{
|
||||
"entity_component": {
|
||||
"_": {
|
||||
"default": "mdi:lightbulb"
|
||||
"default": "mdi:lightbulb",
|
||||
"state_attributes": {
|
||||
"effect": {
|
||||
"default": "mdi:circle-medium",
|
||||
"state": {
|
||||
"off": "mdi:star-off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
|
@ -93,7 +93,10 @@
|
||||
"name": "Color temperature (Kelvin)"
|
||||
},
|
||||
"effect": {
|
||||
"name": "Effect"
|
||||
"name": "Effect",
|
||||
"state": {
|
||||
"off": "[%key:common::state::off%]"
|
||||
}
|
||||
},
|
||||
"effect_list": {
|
||||
"name": "Available effects"
|
||||
|
@ -7,6 +7,6 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["linkplay"],
|
||||
"requirements": ["python-linkplay==0.2.1"],
|
||||
"requirements": ["python-linkplay==0.2.2"],
|
||||
"zeroconf": ["_linkplay._tcp.local."]
|
||||
}
|
||||
|
@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["ical"],
|
||||
"requirements": ["ical==9.0.1"]
|
||||
"requirements": ["ical==9.0.3"]
|
||||
}
|
||||
|
@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_todo",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["ical==9.0.1"]
|
||||
"requirements": ["ical==9.0.3"]
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import frontend, websocket_api
|
||||
from homeassistant.components import frontend, onboarding, websocket_api
|
||||
from homeassistant.config import (
|
||||
async_hass_config_yaml,
|
||||
async_process_component_and_handle_errors,
|
||||
@ -17,6 +17,7 @@ from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import collection, config_validation as cv
|
||||
from homeassistant.helpers.frame import report_usage
|
||||
from homeassistant.helpers.service import async_register_admin_service
|
||||
from homeassistant.helpers.translation import async_get_translations
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import async_get_integration
|
||||
from homeassistant.util import slugify
|
||||
@ -282,6 +283,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
STORAGE_DASHBOARD_UPDATE_FIELDS,
|
||||
).async_setup(hass)
|
||||
|
||||
def create_map_dashboard() -> None:
|
||||
"""Create a map dashboard."""
|
||||
hass.async_create_task(_create_map_dashboard(hass, dashboards_collection))
|
||||
|
||||
if not onboarding.async_is_onboarded(hass):
|
||||
onboarding.async_add_listener(hass, create_map_dashboard)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@ -323,3 +331,25 @@ def _register_panel(
|
||||
kwargs["sidebar_icon"] = config.get(CONF_ICON, DEFAULT_ICON)
|
||||
|
||||
frontend.async_register_built_in_panel(hass, DOMAIN, **kwargs)
|
||||
|
||||
|
||||
async def _create_map_dashboard(
|
||||
hass: HomeAssistant, dashboards_collection: dashboard.DashboardsCollection
|
||||
) -> None:
|
||||
"""Create a map dashboard."""
|
||||
translations = await async_get_translations(
|
||||
hass, hass.config.language, "dashboard", {onboarding.DOMAIN}
|
||||
)
|
||||
title = translations["component.onboarding.dashboard.map.title"]
|
||||
|
||||
await dashboards_collection.async_create_item(
|
||||
{
|
||||
CONF_ALLOW_SINGLE_WORD: True,
|
||||
CONF_ICON: "mdi:map",
|
||||
CONF_TITLE: title,
|
||||
CONF_URL_PATH: "map",
|
||||
}
|
||||
)
|
||||
|
||||
map_store = hass.data[LOVELACE_DATA].dashboards["map"]
|
||||
await map_store.async_save({"strategy": {"type": "map"}})
|
||||
|
@ -69,7 +69,7 @@ class MatterEventEntity(MatterEntity, EventEntity):
|
||||
max_presses_supported = self.get_matter_attribute_value(
|
||||
clusters.Switch.Attributes.MultiPressMax
|
||||
)
|
||||
max_presses_supported = min(max_presses_supported or 1, 8)
|
||||
max_presses_supported = min(max_presses_supported or 2, 8)
|
||||
for i in range(max_presses_supported):
|
||||
event_types.append(f"multi_press_{i + 1}") # noqa: PERF401
|
||||
elif feature_map & SwitchFeature.kMomentarySwitch:
|
||||
|
@ -23,7 +23,11 @@ from homeassistant.helpers.network import (
|
||||
from .const import CONTENT_AUTH_EXPIRY_TIME, MediaClass, MediaType
|
||||
|
||||
# Paths that we don't need to sign
|
||||
PATHS_WITHOUT_AUTH = ("/api/tts_proxy/", "/api/esphome/ffmpeg_proxy/")
|
||||
PATHS_WITHOUT_AUTH = (
|
||||
"/api/tts_proxy/",
|
||||
"/api/esphome/ffmpeg_proxy/",
|
||||
"/api/assist_satellite/static/",
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
|
@ -6,7 +6,7 @@
|
||||
"description": "[%key:component::bluetooth::config::step::user::description%]",
|
||||
"data": {
|
||||
"address": "[%key:common::config_flow::data::device%]",
|
||||
"medium_type": "Medium Type"
|
||||
"medium_type": "Medium type"
|
||||
}
|
||||
},
|
||||
"bluetooth_confirm": {
|
||||
|
@ -153,7 +153,6 @@ from .util import (
|
||||
learn_more_url,
|
||||
valid_birth_will,
|
||||
valid_publish_topic,
|
||||
valid_qos_schema,
|
||||
valid_subscribe_topic,
|
||||
valid_subscribe_topic_template,
|
||||
)
|
||||
@ -182,7 +181,6 @@ PASSWORD_SELECTOR = TextSelector(TextSelectorConfig(type=TextSelectorType.PASSWO
|
||||
QOS_SELECTOR = NumberSelector(
|
||||
NumberSelectorConfig(mode=NumberSelectorMode.BOX, min=0, max=2)
|
||||
)
|
||||
QOS_DATA_SCHEMA = vol.All(QOS_SELECTOR, valid_qos_schema)
|
||||
KEEPALIVE_SELECTOR = vol.All(
|
||||
NumberSelector(
|
||||
NumberSelectorConfig(
|
||||
@ -1145,7 +1143,7 @@ class MQTTOptionsFlowHandler(OptionsFlow):
|
||||
"birth_payload", description={"suggested_value": birth[CONF_PAYLOAD]}
|
||||
)
|
||||
] = TEXT_SELECTOR
|
||||
fields[vol.Optional("birth_qos", default=birth[ATTR_QOS])] = QOS_DATA_SCHEMA
|
||||
fields[vol.Optional("birth_qos", default=birth[ATTR_QOS])] = QOS_SELECTOR
|
||||
fields[vol.Optional("birth_retain", default=birth[ATTR_RETAIN])] = (
|
||||
BOOLEAN_SELECTOR
|
||||
)
|
||||
@ -1168,7 +1166,7 @@ class MQTTOptionsFlowHandler(OptionsFlow):
|
||||
"will_payload", description={"suggested_value": will[CONF_PAYLOAD]}
|
||||
)
|
||||
] = TEXT_SELECTOR
|
||||
fields[vol.Optional("will_qos", default=will[ATTR_QOS])] = QOS_DATA_SCHEMA
|
||||
fields[vol.Optional("will_qos", default=will[ATTR_QOS])] = QOS_SELECTOR
|
||||
fields[vol.Optional("will_retain", default=will[ATTR_RETAIN])] = (
|
||||
BOOLEAN_SELECTOR
|
||||
)
|
||||
@ -1269,13 +1267,9 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
|
||||
reconfig=True,
|
||||
)
|
||||
if user_input is not None:
|
||||
merged_user_input, errors = validate_user_input(
|
||||
user_input, MQTT_DEVICE_PLATFORM_FIELDS
|
||||
)
|
||||
_, errors = validate_user_input(user_input, MQTT_DEVICE_PLATFORM_FIELDS)
|
||||
if not errors:
|
||||
self._subentry_data[CONF_DEVICE] = cast(
|
||||
MqttDeviceData, merged_user_input
|
||||
)
|
||||
self._subentry_data[CONF_DEVICE] = cast(MqttDeviceData, user_input)
|
||||
if self.source == SOURCE_RECONFIGURE:
|
||||
return await self.async_step_summary_menu()
|
||||
return await self.async_step_entity()
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user