Compare commits

...

77 Commits

Author SHA1 Message Date
Franck Nijhof
7766649304
Bump version to 2025.4.0b9 2025-03-29 17:50:46 +00:00
Simone Chemelli
07e9020dfa
Fix immediate state update for Comelit (#141735) 2025-03-29 17:50:36 +00:00
J. Diego Rodríguez Royo
f504a759e0
Set Home Connect program action field as not required (#141729)
* Set Home Connect program action field as not required

* Remove required field

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-03-29 17:50:32 +00:00
Joost Lekkerkerker
9927de4801
Only trigger events on button updates in SmartThings (#141720)
Only trigger events on button updates
2025-03-29 17:50:29 +00:00
Joost Lekkerkerker
1244fc4682
Only link the parent device if known in SmartThings (#141719)
Only link the parent device if we know the parent device
2025-03-29 17:50:26 +00:00
Norbert Rittel
e77a1b12f7
Sentence-case "Medium type" in mopeka (#141718) 2025-03-29 17:50:22 +00:00
J. Nick Koston
5459daaa10
Fix ESPHome entities not being removed when the ESPHome config removes an entire platform (#141708)
* Fix old ESPHome entities not being removed when configuration changes

fixes #140756

* make sure all callbacks fire

* make sure all callbacks fire

* make sure all callbacks fire

* make sure all callbacks fire

* revert

* cover
2025-03-29 17:50:18 +00:00
J. Nick Koston
400131df78
Fix ESPHome update entities being loaded before device_info is available (#141704)
* Fix ESPHome update entities being loaded before device_info is available

Since we load platforms when restoring config, the update
platform could be loaded before the connection to the
device was finished which meant device_info could still
be empty. Wait until device_info is available to
load the update platform.

fixes #135906

* Apply suggestions from code review

* move comment

* Update entry_data.py

Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>

---------

Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
2025-03-29 17:50:15 +00:00
Franck Nijhof
28e1843ff9
Fix Tuya tdq category to pick up temp & humid (#141698) 2025-03-29 17:50:12 +00:00
Franck Nijhof
df777318d1
Handle invalid JSON errors in AirNow (#141695) 2025-03-29 17:50:08 +00:00
Jan Bouwhuis
6ad5e9e89c
Improve MQTT translation strings (#141691)
* Improve MQTT options translation string

* more improvements
2025-03-29 17:50:05 +00:00
Norbert Rittel
a0bd8deee9
Replace "country" with common string in holiday (#141687) 2025-03-29 17:50:01 +00:00
Marcel van der Veldt
405cbd6a00
Always set pause feature on Music Assistant mediaplayers (#141686) 2025-03-29 17:49:58 +00:00
Marcel van der Veldt
3e0eb5ab2c
Bump music assistant client to 1.2.0 (#141668)
* Bump music assistant client to 1.2.0

* Update test fixtures
2025-03-29 17:49:55 +00:00
Norbert Rittel
fad75a70b6
Add a common string for "country" (#141653) 2025-03-29 17:49:52 +00:00
Josef Zweck
d9720283df
Add unkown to uncalibrated state for tedee (#141262) 2025-03-29 17:49:46 +00:00
Franck Nijhof
14eed1778b
Bump version to 2025.4.0b8 2025-03-28 20:46:26 +00:00
Norbert Rittel
049aaa7e8b
Fix grammar / sentence-casing in workday (#141682)
* Fix grammar / sentence-casing in `workday`

Also replace "country" with common string.

* Add two more references

* Fix second data description reference

* Add "given" to action description for better translations
2025-03-28 20:46:17 +00:00
J. Nick Koston
35717e8216
Increase websocket_api allowed peak time to 10s (#141680)
* Increase websocket_api allowed peak time to 10s

fixes #141624

During integration reload or startup, we can end up sending a message for
each entity being created for integrations that create them from an external
source (ie MQTT) because the messages come in one at a time. This can overload
the loop and/or client for more than 5s. While we have done significant work
to optimize for this path, we are at the limit at what we can expect clients
to be able to process in the time window, so increase the time window.

* adjust test
2025-03-28 20:46:13 +00:00
Franck Nijhof
2a081abc18
Fix camera proxy with sole image quality settings (#141676) 2025-03-28 20:46:10 +00:00
puddly
b7f29c7358
Handle all firmware types for ZBT-1 and Yellow update entities (#141674)
Handle other firmware types
2025-03-28 20:46:06 +00:00
Jason Hunter
3bb6373df5
Update Duke Energy package to fix integration (#141669)
* Update Duke Energy package to fix integration

* fix tests
2025-03-28 20:46:03 +00:00
Michael Hansen
e1b4edec50
Bump intents and always prefer more literal text (#141663) 2025-03-28 20:46:00 +00:00
puddly
147bee57e1
Include ZBT-1 and Yellow in device registry (#141623)
* Add the Yellow and ZBT-1 to the device registry

* Unload platforms

* Fix unit tests

* Rename the Yellow update entity to `Radio firmware`

* Rename `EmberZNet` to `EmberZNet Zigbee`

* Prefix the `sw_version` with the firmware type and clean up

* Fix unit tests

* Remove unnecessary `always_update=False` from data update coordinator
2025-03-28 20:45:56 +00:00
Erwin Douna
fcdaea64da
Tado add proper off state (#135480)
* Add proper off state

* Remove current temp

* Add default frost temp
2025-03-28 20:45:53 +00:00
Franck Nijhof
d1512d46be
Bump version to 2025.4.0b7 2025-03-28 16:00:45 +00:00
Bram Kragten
0be7db6270
Update frontend to 20250328.0 (#141659) 2025-03-28 15:09:56 +00:00
Paulus Schoutsen
2af0282725
Enable the message box on default for satelitte announcement actions (#141654) 2025-03-28 15:09:51 +00:00
Franck Nijhof
ff458c8417
Bump version to 2025.4.0b6 2025-03-28 15:04:34 +00:00
Franck Nijhof
cc93152ff0
Fix ESPHome event entity staying unavailable (#141650) 2025-03-28 14:05:40 +00:00
Paulus Schoutsen
9965f01609
Ensure connection test sound has no preannouncement (#141647) 2025-03-28 14:05:37 +00:00
Jan Bouwhuis
e9c76ce694
Fix duplicate 'device' term in MQTT translation strings (#141646)
* Fix duplicate 'device' from MQTT translation strings

* Update homeassistant/components/mqtt/strings.json
2025-03-28 14:05:34 +00:00
Norbert Rittel
58ab7d350d
Fix sentence-casing in airvisual user strings (#141632) 2025-03-28 14:05:30 +00:00
Nick Pesce
e4d6e20ebd
Use correct default value for multi press buttons in the Matter integration (#141630)
* Respect the min 2 constraint for the switch MultiPressMax attribute

* Update test_event.py

* Update generic_switch_multi.json

* Fix issue and update tests
2025-03-28 14:05:27 +00:00
Tsvi Mostovicz
45e273897a
Jewish calendar match omer service variables requirement to documentation (#141620)
The documentation and the omer schema require a Nusach to be specified, but the YAML misses that requirement
2025-03-28 14:05:23 +00:00
Jan Bouwhuis
d9ec7142d7
Fix volatile_organic_compounds_parts translation string to be referenced for MQTT subentries device class selector (#141618)
* Fix ` volatile_organic_compounds_parts` translation string to be referenced for MQTT subentries device class selector

* Fix tests
2025-03-28 14:05:20 +00:00
Petro31
e162499267
Fix an issue with the switch preview in beta (#141617)
Fix an issue with the switch preview
2025-03-28 14:05:16 +00:00
Jan-Philipp Benecke
67f21429e3
Bump aiowebdav2 to 0.4.4 (#141615) 2025-03-28 14:05:12 +00:00
J. Nick Koston
a0563f06c9
Fix zeroconf logging level not being respected (#141601)
Removes an old logging workaround that is no longer needed

fixes #141558
2025-03-28 14:05:05 +00:00
Luke Lashley
e7c4fdc8bb
Bump Python-Snoo to 0.6.5 (#141599)
* Bump Python-Snoo to 0.6.5

* add to event_types
2025-03-28 14:05:00 +00:00
Norbert Rittel
c490e350bc
Make names of switch entities in gree consistent with docs (#141580) 2025-03-28 14:04:56 +00:00
Robert Resch
e11409ef99
Reverts #141363 "Deprecate SmartThings machine state sensors" (#141573)
Reverts #141363
2025-03-28 14:04:52 +00:00
Joost Lekkerkerker
5c8e415a76
Add default string and icon for light effect off (#141567) 2025-03-28 14:04:49 +00:00
alorente
e795fb9497
Fix missing response for queued mode scripts (#141460) 2025-03-28 14:04:45 +00:00
Norbert Rittel
d0afabb85c
Fix misleading friendly names of pvoutput sensors (#141312)
* Fix misleading friendly names of `pvoutput` sensors

* Update test_sensor.py

* Update test_sensor.py - prettier
2025-03-28 14:04:41 +00:00
Franck Nijhof
4f3e8e9b94
Bump version to 2025.4.0b5 2025-03-27 20:03:14 +00:00
Paul Bottein
46c1cbbc9c
Update frontend to 20250327.1 (#141596) 2025-03-27 20:03:01 +00:00
Simon Lamon
8d9a4ea278
Fix typing error in NMBS (#141589)
Fix typing error
2025-03-27 20:02:58 +00:00
Jan-Philipp Benecke
22c83e2393
Bump aiowebdav2 to 0.4.3 (#141586) 2025-03-27 20:02:55 +00:00
Joost Lekkerkerker
c83a75f6f9
Add brand for Bosch (#141561) 2025-03-27 20:02:51 +00:00
Franck Nijhof
841c727112
Bump version to 2025.4.0b4 2025-03-27 16:59:36 +00:00
Bram Kragten
d8c9655bfd
Update frontend to 20250327.0 (#141585) 2025-03-27 16:59:29 +00:00
Erik Montnemery
942ed89cc4
Revert "Promote after dependencies in bootstrap" (#141584)
Revert "Promote after dependencies in bootstrap (#140352)"

This reverts commit 376604096049ac2388a1c9d23c578402acbce0b5.
2025-03-27 16:59:25 +00:00
Franck Nijhof
a1fe6b9cf3
Bump version to 2025.4.0b3 2025-03-27 15:38:31 +00:00
Luke Lashley
2567181cc2
Better handle Roborock discovery (#141575) 2025-03-27 15:38:24 +00:00
Joost Lekkerkerker
028e4f6029
Also migrate completion time entities in SmartThings (#141572) 2025-03-27 15:38:21 +00:00
Martin Hjelmare
b82e1a9bef
Handle cloud subscription expired for backup upload (#141564)
Handle cloud backup subscription expired for upload
2025-03-27 15:38:18 +00:00
Joost Lekkerkerker
438f226c31
Add icons to hue effects (#141559) 2025-03-27 15:38:15 +00:00
Erwin Douna
2f139e3cb1
Tado fix HomeKit flow (#141525)
* Initial commit

* Fix

* Fix

---------

Co-authored-by: Joostlek <joostlek@outlook.com>
2025-03-27 15:38:07 +00:00
Franck Nijhof
5d75e96fbf
Bump version to 2025.4.0b2 2025-03-27 10:19:35 +00:00
Norbert Rittel
dcf2ec5c37
Fix sentence-casing in konnected strings, replace "override" with "custom" (#141553)
Fix sentence-casing in `konnected`strings, replace "Override" with "Custom"

Make string consistent with HA standards.

As "Override" can be misunderstood as the verb, replace it with "Custom".
2025-03-27 10:19:22 +00:00
Simon Lamon
2431e1ba98
Bump linkplay to v0.2.2 (#141542)
Bump linkplay
2025-03-27 10:19:18 +00:00
Thomas55555
4ead108c15
Handle webcal prefix in remote calendar (#141541)
Handel webcal prefix in remote calendar
2025-03-27 10:19:14 +00:00
Michael Hansen
ec8363fa49
Add default preannounce sound to Assist satellites (#141522)
* Add default preannounce sound

* Allow None to disable sound

* Register static path instead of HTTP view

* Fix path

---------

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2025-03-27 10:19:09 +00:00
J. Diego Rodríguez Royo
e7ff0a3f8b
Improve some Home Connect deprecations (#141508) 2025-03-27 10:19:06 +00:00
Ivan Lopez Hernandez
f4c0eb4189
Initialize google.genai.Client in the executor (#141432)
* Intialize the client on an executor thread

* Fix MyPy error

* MyPy error

* Exception error

* Fix ruff

* Update __init__.py

---------

Co-authored-by: tronikos <tronikos@users.noreply.github.com>
2025-03-27 10:19:02 +00:00
Manu
b1ee5a76e1
Support for upcoming pyLoad-ng release in pyLoad integration (#141297)
Fix extra key `proxy` in pyLoad
2025-03-27 10:18:58 +00:00
Norbert Rittel
6b9e8c301b
Fix wrong friendly name for storage_power in solaredge (#141269)
* Fix wrong friendly name for `storage_power` in `solaredge`

"Stored power" is a contradiction in itself.
You can only store energy.

* Two additional spelling fixes

* Sentence-case "site"
2025-03-27 10:18:53 +00:00
Franck Nijhof
89c3266c7e
Bump version to 2025.4.0b1 2025-03-26 23:21:26 +00:00
Jan Bouwhuis
cff0a632e8
Fix QoS schema issue in MQTT subentries (#141531) 2025-03-26 23:21:17 +00:00
Jan Bouwhuis
e04d8557ae
Fix MQTT options flow QoS selector can not serialize (#141528) 2025-03-26 23:21:14 +00:00
Thomas55555
ca6286f241
Fix work area sensor for Husqvarna Automower (#141527)
* Fix work area sensor for Husqvarna Automower

* simplify
2025-03-26 23:21:10 +00:00
Robert Resch
35bcc9d5af
Show box for Smartthings rise number entity (#141526) 2025-03-26 23:21:07 +00:00
Joost Lekkerkerker
25b45ce867
Sort SmartThings devices to be created by parent device id (#141515) 2025-03-26 23:21:03 +00:00
Robert Resch
d568209bd5
Bump deebot-client to 12.4.0 (#141501) 2025-03-26 23:21:00 +00:00
Simone Chemelli
8a43e8af9e
Fix refresh state for Comelit alarm (#141370) 2025-03-26 23:20:56 +00:00
Franck Nijhof
785e5b2c16
Bump version to 2025.4.0b0 2025-03-26 17:41:03 +00:00
123 changed files with 1559 additions and 422 deletions

View File

@ -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

View File

@ -0,0 +1,5 @@
{
"domain": "bosch",
"name": "Bosch",
"integrations": ["bosch_alarm", "bosch_shc", "home_connect"]
}

View File

@ -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:

View File

@ -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": {

View File

@ -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,7 +60,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
{
vol.Optional("message"): str,
vol.Optional("media_id"): str,
vol.Optional("preannounce_media_id"): str,
vol.Optional("preannounce_media_id"): vol.Any(str, None),
}
),
cv.has_at_least_one_key("message", "media_id"),
@ -71,7 +75,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
{
vol.Optional("start_message"): str,
vol.Optional("start_media_id"): str,
vol.Optional("preannounce_media_id"): str,
vol.Optional("preannounce_media_id"): vol.Any(str, None),
vol.Optional("extra_system_prompt"): str,
}
),
@ -84,6 +88,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

View File

@ -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."""

View File

@ -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,7 @@ class AssistSatelliteEntity(entity.Entity):
self,
message: str | None = None,
media_id: str | None = None,
preannounce_media_id: str | None = None,
preannounce_media_id: str | None = PREANNOUNCE_URL,
) -> None:
"""Play and show an announcement on the satellite.
@ -190,7 +190,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_media_id is provided, it overrides the default sound.
If preannounce_media_id is None, no sound is played.
Calls async_announce with message and media id.
"""
@ -228,7 +229,7 @@ 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_media_id: str | None = PREANNOUNCE_URL,
) -> None:
"""Start a conversation from the satellite.
@ -239,6 +240,7 @@ class AssistSatelliteEntity(entity.Entity):
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_media_id is None, no sound is played.
Calls async_start_conversation.
"""

View File

@ -8,6 +8,7 @@ announce:
message:
required: false
example: "Time to wake up!"
default: ""
selector:
text:
media_id:
@ -28,6 +29,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:

View File

@ -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_media_id=None,
),
f"assist_satellite_connection_test_{msg['entity_id']}",
)

View File

@ -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

View File

@ -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]
)

View File

@ -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."""

View File

@ -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."""

View File

@ -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."""

View File

@ -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)

View File

@ -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"]
}

View File

@ -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,

View File

@ -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"]
}

View File

@ -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.4.0"]
}

View File

@ -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:

View File

@ -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,

View File

@ -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==20250328.0"]
}

View File

@ -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}},

View File

@ -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"

View File

@ -8,7 +8,7 @@
"step": {
"user": {
"data": {
"country": "Country"
"country": "[%key:common::config_flow::data::country%]"
}
},
"options": {

View File

@ -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={

View File

@ -64,7 +64,6 @@ set_program_and_options:
- selected_program
program:
example: dishcare_dishwasher_program_auto2
required: true
selector:
select:
mode: dropdown

View File

@ -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",

View File

@ -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:

View File

@ -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

View File

@ -199,7 +199,7 @@ class BaseFirmwareUpdateEntity(
# 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

View File

@ -15,14 +15,13 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a Home Assistant SkyConnect config entry."""
await hass.config_entries.async_forward_entry_setups(entry, ["update"])
return True
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

View File

@ -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"],
@ -130,6 +168,7 @@ class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
"""SkyConnect firmware update entity."""
bootloader_reset_type = None
_attr_has_entity_name = True
def __init__(
self,
@ -141,8 +180,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 +204,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."""

View File

@ -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

View File

@ -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 = {

View File

@ -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": {
"firmware": {
"name": "Radio firmware"
}
}
}
}

View File

@ -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,7 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[
ApplicationType | None, FirmwareUpdateEntityDescription
] = {
ApplicationType.EZSP: FirmwareUpdateEntityDescription(
key="firmware",
key="radio_firmware",
display_precision=0,
device_class=UpdateDeviceClass.FIRMWARE,
entity_category=EntityCategory.CONFIG,
@ -47,10 +51,10 @@ 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",
display_precision=0,
device_class=UpdateDeviceClass.FIRMWARE,
entity_category=EntityCategory.CONFIG,
@ -60,12 +64,34 @@ FIRMWARE_ENTITY_DESCRIPTIONS: dict[
expected_firmware_type=ApplicationType.SPINEL,
firmware_name="OpenThread RCP",
),
None: FirmwareUpdateEntityDescription(
ApplicationType.CPC: FirmwareUpdateEntityDescription(
key="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="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="radio_firmware",
display_precision=0,
device_class=UpdateDeviceClass.FIRMWARE,
entity_category=EntityCategory.CONFIG,
version_parser=lambda fw: fw,
fw_type=None,
version_key=None,
expected_firmware_type=None,
@ -82,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=RADIO_DEVICE,
@ -135,6 +168,7 @@ class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
"""Yellow firmware update entity."""
bootloader_reset_type = "yellow" # Triggers a GPIO reset
_attr_has_entity_name = True
def __init__(
self,
@ -145,8 +179,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 +197,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."""

View File

@ -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"

View File

@ -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

View File

@ -6,6 +6,7 @@ count_omer:
selector:
date:
nusach:
required: true
example: "sfarad"
default: "sfarad"
selector:

View File

@ -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%]"

View File

@ -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": {

View File

@ -93,7 +93,10 @@
"name": "Color temperature (Kelvin)"
},
"effect": {
"name": "Effect"
"name": "Effect",
"state": {
"off": "[%key:common::state::off%]"
}
},
"effect_list": {
"name": "Available effects"

View File

@ -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."]
}

View File

@ -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:

View File

@ -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

View File

@ -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": {

View File

@ -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()

View File

@ -219,10 +219,10 @@
"options": "Add option"
},
"data_description": {
"device_class": "The device class of the {platform} entity. [Learn more.]({url}#device_class)",
"state_class": "The [state_class](https://developers.home-assistant.io/docs/core/entity/sensor/#available-state-classes) of the sensor. [Learn more.]({url}#state_class)",
"device_class": "The Device class of the {platform} entity. [Learn more.]({url}#device_class)",
"state_class": "The [State class](https://developers.home-assistant.io/docs/core/entity/sensor/#available-state-classes) of the sensor. [Learn more.]({url}#state_class)",
"unit_of_measurement": "Defines the unit of measurement of the sensor, if any.",
"options": "Options for allowed sensor state values. The sensors device_class must be set to Enumeration. The options option cannot be used together with State Class or Unit of measurement."
"options": "Options for allowed sensor state values. The sensors Device class must be set to Enumeration. The 'Options' setting cannot be used together with State class or Unit of measurement."
},
"sections": {
"advanced_settings": {
@ -285,9 +285,9 @@
"invalid_uom": "The unit of measurement \"{unit_of_measurement}\" is not supported by the selected device class, please either remove the device class, select a device class which supports \"{unit_of_measurement}\", or pick a supported unit of measurement from the list",
"invalid_url": "Invalid URL",
"options_not_allowed_with_state_class_or_uom": "The 'Options' setting is not allowed when state class or unit of measurement are used",
"options_device_class_enum": "The 'Options' setting must be used with the Enumeration device class'. If you continue, the existing options will be reset",
"options_device_class_enum": "The 'Options' setting must be used with the Enumeration device class. If you continue, the existing options will be reset",
"options_with_enum_device_class": "Configure options for the enumeration sensor",
"uom_required_for_device_class": "The selected device device class requires a unit"
"uom_required_for_device_class": "The selected device class requires a unit"
}
}
},
@ -453,7 +453,7 @@
"temperature": "[%key:component::sensor::entity_component::temperature::name%]",
"timestamp": "[%key:component::sensor::entity_component::timestamp::name%]",
"volatile_organic_compounds": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]",
"volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]",
"volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds_parts::name%]",
"voltage": "[%key:component::sensor::entity_component::voltage::name%]",
"volume": "[%key:component::sensor::entity_component::volume::name%]",
"volume_flow_rate": "[%key:component::sensor::entity_component::volume_flow_rate::name%]",

View File

@ -7,6 +7,6 @@
"documentation": "https://www.home-assistant.io/integrations/music_assistant",
"iot_class": "local_push",
"loggers": ["music_assistant"],
"requirements": ["music-assistant-client==1.1.1"],
"requirements": ["music-assistant-client==1.2.0"],
"zeroconf": ["_mass._tcp.local."]
}

View File

@ -94,6 +94,12 @@ SUPPORTED_FEATURES_BASE = (
| MediaPlayerEntityFeature.MEDIA_ENQUEUE
| MediaPlayerEntityFeature.MEDIA_ANNOUNCE
| MediaPlayerEntityFeature.SEEK
# we always add pause support,
# regardless if the underlying player actually natively supports pause
# because the MA behavior is to internally handle pause with stop
# (and a resume position) and we'd like to keep the UX consistent
# background info: https://github.com/home-assistant/core/issues/140118
| MediaPlayerEntityFeature.PAUSE
)
QUEUE_OPTION_MAP = {
@ -697,8 +703,6 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity):
supported_features = SUPPORTED_FEATURES_BASE
if PlayerFeature.SET_MEMBERS in self.player.supported_features:
supported_features |= MediaPlayerEntityFeature.GROUPING
if PlayerFeature.PAUSE in self.player.supported_features:
supported_features |= MediaPlayerEntityFeature.PAUSE
if self.player.mute_control != PLAYER_CONTROL_NONE:
supported_features |= MediaPlayerEntityFeature.VOLUME_MUTE
if self.player.volume_control != PLAYER_CONTROL_NONE:

View File

@ -360,7 +360,7 @@ class NMBSSensor(SensorEntity):
attrs[ATTR_LONGITUDE] = self.station_coordinates[1]
if self.is_via_connection and not self._excl_vias:
via = self._attrs.vias.via[0]
via = self._attrs.vias[0]
attrs["via"] = via.station
attrs["via_arrival_platform"] = via.arrival.platform

View File

@ -104,6 +104,15 @@ def _resize_image(image, opts):
new_width = opts.max_width
(old_width, old_height) = img.size
old_size = len(image)
# If no max_width specified, only apply quality changes if requested
if new_width is None:
if opts.quality is None:
return image
imgbuf = io.BytesIO()
img.save(imgbuf, "JPEG", optimize=True, quality=quality)
return imgbuf.getvalue()
if old_width <= new_width:
if opts.quality is None:
_LOGGER.debug("Image is smaller-than/equal-to requested width")

View File

@ -27,19 +27,19 @@
"entity": {
"sensor": {
"energy_consumption": {
"name": "Energy consumed"
"name": "Energy consumption"
},
"energy_generation": {
"name": "Energy generated"
"name": "Energy generation"
},
"efficiency": {
"name": "Efficiency"
},
"power_consumption": {
"name": "Power consumed"
"name": "Power consumption"
},
"power_generation": {
"name": "Power generated"
"name": "Power generation"
}
}
}

View File

@ -31,6 +31,7 @@ class PyLoadData:
download: bool
reconnect: bool
captcha: bool | None = None
proxy: bool | None = None
free_space: int

View File

@ -42,6 +42,10 @@ class RemoteCalendarConfigFlow(ConfigFlow, domain=DOMAIN):
self._async_abort_entries_match(
{CONF_CALENDAR_NAME: user_input[CONF_CALENDAR_NAME]}
)
if user_input[CONF_URL].startswith("webcal://"):
user_input[CONF_URL] = user_input[CONF_URL].replace(
"webcal://", "https://", 1
)
self._async_abort_entries_match({CONF_URL: user_input[CONF_URL]})
client = get_async_client(self.hass)
try:

View File

@ -143,6 +143,7 @@ class RoborockFlowHandler(ConfigFlow, domain=DOMAIN):
self, discovery_info: DhcpServiceInfo
) -> ConfigFlowResult:
"""Handle a flow started by a dhcp discovery."""
await self._async_handle_discovery_without_unique_id()
device_registry = dr.async_get(self.hass)
device = device_registry.async_get_device(
connections={

View File

@ -278,10 +278,10 @@
"name": "Timestamp"
},
"volatile_organic_compounds": {
"name": "VOCs"
"name": "Volatile organic compounds"
},
"volatile_organic_compounds_parts": {
"name": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]"
"name": "Volatile organic compounds parts"
},
"voltage": {
"name": "Voltage"

View File

@ -352,7 +352,10 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return {
"new_unique_id": f"{device_id}_{MAIN}_{Capability.THREE_AXIS}_{Attribute.THREE_AXIS}_{new_attribute}",
}
if attribute == Attribute.MACHINE_STATE:
if attribute in {
Attribute.MACHINE_STATE,
Attribute.COMPLETION_TIME,
}:
capability = determine_machine_type(
hass, entry.entry_id, device_id
)
@ -410,7 +413,9 @@ def create_devices(
rooms: dict[str, str],
) -> None:
"""Create devices in the device registry."""
for device in devices.values():
for device in sorted(
devices.values(), key=lambda d: d.device.parent_device_id or ""
):
kwargs: dict[str, Any] = {}
if device.device.hub is not None:
kwargs = {
@ -421,7 +426,7 @@ def create_devices(
kwargs[ATTR_CONNECTIONS] = {
(dr.CONNECTION_NETWORK_MAC, device.device.hub.mac_address)
}
if device.device.parent_device_id:
if device.device.parent_device_id and device.device.parent_device_id in devices:
kwargs[ATTR_VIA_DEVICE] = (DOMAIN, device.device.parent_device_id)
if (ocf := device.device.ocf) is not None:
kwargs.update(

View File

@ -58,5 +58,6 @@ class SmartThingsButtonEvent(SmartThingsEntity, EventEntity):
)
def _update_handler(self, event: DeviceEvent) -> None:
self._trigger_event(cast(str, event.value))
self.async_write_ha_state()
if event.attribute is Attribute.BUTTON:
self._trigger_event(cast(str, event.value))
super()._update_handler(event)

View File

@ -4,7 +4,7 @@ from __future__ import annotations
from pysmartthings import Attribute, Capability, Command, SmartThings
from homeassistant.components.number import NumberEntity
from homeassistant.components.number import NumberEntity, NumberMode
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@ -32,6 +32,7 @@ class SmartThingsWasherRinseCyclesNumberEntity(SmartThingsEntity, NumberEntity):
_attr_translation_key = "washer_rinse_cycles"
_attr_native_step = 1.0
_attr_mode = NumberMode.BOX
def __init__(self, client: SmartThings, device: FullDevice) -> None:
"""Initialize the instance."""

View File

@ -331,7 +331,6 @@ CAPABILITY_TO_SENSORS: dict[
translation_key="dryer_machine_state",
options=WASHER_OPTIONS,
device_class=SensorDeviceClass.ENUM,
deprecated=lambda _: "machine_state",
)
],
Attribute.DRYER_JOB_STATE: [
@ -966,7 +965,6 @@ CAPABILITY_TO_SENSORS: dict[
translation_key="washer_machine_state",
options=WASHER_OPTIONS,
device_class=SensorDeviceClass.ENUM,
deprecated=lambda _: "machine_state",
)
],
Attribute.WASHER_JOB_STATE: [

View File

@ -487,10 +487,6 @@
"title": "Deprecated refrigerator door binary sensor detected in some automations or scripts",
"description": "The refrigerator door binary sensor `{entity}` is deprecated and is used in the following automations or scripts:\n{items}\n\nSeparate entities for cooler and freezer door are available and should be used going forward. Please use them in the above automations or scripts to fix this issue."
},
"deprecated_machine_state": {
"title": "Deprecated machine state sensor detected in some automations or scripts",
"description": "The machine state sensor `{entity}` is deprecated and is used in the following automations or scripts:\n{items}\n\nA select entity is now available for the machine state and should be used going forward. Please use the new select entity in the above automations or scripts to fix this issue."
},
"deprecated_switch_appliance": {
"title": "Deprecated switch detected in some automations or scripts",
"description": "The switch `{entity}` is deprecated because the actions did not work, so it has been replaced with a binary sensor instead.\n\nThe switch was used in the following automations or scripts:\n{items}\n\nPlease use the new binary sensor in the above automations or scripts to fix this issue."

View File

@ -31,6 +31,7 @@ async def async_setup_entry(
"power",
"status_requested",
"sticky_white_noise_updated",
"config_change",
],
),
)

View File

@ -7,5 +7,5 @@
"iot_class": "cloud_push",
"loggers": ["snoo"],
"quality_scale": "bronze",
"requirements": ["python-snoo==0.6.4"]
"requirements": ["python-snoo==0.6.5"]
}

View File

@ -55,7 +55,8 @@
"activity": "Activity press",
"power": "Power button pressed",
"status_requested": "Status requested",
"sticky_white_noise_updated": "Sleepytime sounds updated"
"sticky_white_noise_updated": "Sleepytime sounds updated",
"config_change": "Config changed"
}
}
}

View File

@ -5,7 +5,7 @@
"title": "Define the API parameters for this installation",
"data": {
"name": "The name of this installation",
"site_id": "The SolarEdge site-id",
"site_id": "The SolarEdge site ID",
"api_key": "[%key:common::config_flow::data::api_key%]"
}
}
@ -14,7 +14,7 @@
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]",
"site_not_active": "The site is not active",
"could_not_connect": "Could not connect to the solaredge API"
"could_not_connect": "Could not connect to the SolarEdge API"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
@ -65,7 +65,7 @@
"name": "Grid power"
},
"storage_power": {
"name": "Stored power"
"name": "Storage power"
},
"purchased_energy": {
"name": "Imported energy"

View File

@ -477,11 +477,9 @@ class TadoClimate(TadoZoneEntity, ClimateEntity):
@property
def target_temperature(self) -> float | None:
"""Return the temperature we try to reach."""
# If the target temperature will be None
# if the device is performing an action
# that does not affect the temperature or
# the device is switching states
return self._tado_zone_data.target_temp or self._tado_zone_data.current_temp
if self._current_tado_hvac_mode == CONST_MODE_OFF:
return TADO_DEFAULT_MIN_TEMP
return self._tado_zone_data.target_temp
async def set_timer(
self,

View File

@ -22,10 +22,7 @@ from homeassistant.config_entries import (
)
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.service_info.zeroconf import (
ATTR_PROPERTIES_ID,
ZeroconfServiceInfo,
)
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
from .const import (
CONF_FALLBACK,
@ -164,12 +161,16 @@ class TadoConfigFlow(ConfigFlow, domain=DOMAIN):
self, discovery_info: ZeroconfServiceInfo
) -> ConfigFlowResult:
"""Handle HomeKit discovery."""
self._async_abort_entries_match()
properties = {
key.lower(): value for key, value in discovery_info.properties.items()
}
await self.async_set_unique_id(properties[ATTR_PROPERTIES_ID])
self._abort_if_unique_id_configured()
await self._async_handle_discovery_without_unique_id()
return await self.async_step_homekit_confirm()
async def async_step_homekit_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Prepare for Homekit."""
if user_input is None:
return self.async_show_form(step_id="homekit_confirm")
return await self.async_step_user()
@staticmethod

View File

@ -16,6 +16,10 @@
"title": "Authenticate with Tado",
"description": "You need to reauthenticate with Tado. Press `Submit` to start the authentication process."
},
"homekit": {
"title": "Authenticate with Tado",
"description": "Your device has been discovered and needs to authenticate with Tado. Press `Submit` to start the authentication process."
},
"timeout": {
"description": "The authentication process timed out. Please try again."
}

View File

@ -41,7 +41,7 @@ ENTITIES: tuple[TedeeBinarySensorEntityDescription, ...] = (
TedeeBinarySensorEntityDescription(
key="semi_locked",
translation_key="semi_locked",
is_on_fn=lambda lock: lock.state == TedeeLockState.HALF_OPEN,
is_on_fn=lambda lock: lock.state is TedeeLockState.HALF_OPEN,
entity_category=EntityCategory.DIAGNOSTIC,
),
TedeeBinarySensorEntityDescription(
@ -53,7 +53,10 @@ ENTITIES: tuple[TedeeBinarySensorEntityDescription, ...] = (
TedeeBinarySensorEntityDescription(
key="uncalibrated",
translation_key="uncalibrated",
is_on_fn=lambda lock: lock.state == TedeeLockState.UNCALIBRATED,
is_on_fn=(
lambda lock: lock.state is TedeeLockState.UNCALIBRATED
or lock.state is TedeeLockState.UNKNOWN
),
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,

View File

@ -120,7 +120,7 @@ def rewrite_legacy_to_modern_conf(
return switches
def rewrite_options_to_moder_conf(option_config: dict[str, dict]) -> dict[str, dict]:
def rewrite_options_to_modern_conf(option_config: dict[str, dict]) -> dict[str, dict]:
"""Rewrite option configuration to modern configuration."""
option_config = {**option_config}
@ -189,7 +189,7 @@ async def async_setup_entry(
"""Initialize config entry."""
_options = dict(config_entry.options)
_options.pop("template_type")
_options = rewrite_options_to_moder_conf(_options)
_options = rewrite_options_to_modern_conf(_options)
validated_config = SWITCH_CONFIG_SCHEMA(_options)
async_add_entities([SwitchTemplate(hass, validated_config, config_entry.entry_id)])
@ -199,7 +199,8 @@ def async_create_preview_switch(
hass: HomeAssistant, name: str, config: dict[str, Any]
) -> SwitchTemplate:
"""Create a preview switch."""
validated_config = SWITCH_CONFIG_SCHEMA(config | {CONF_NAME: name})
updated_config = rewrite_options_to_modern_conf(config)
validated_config = SWITCH_CONFIG_SCHEMA(updated_config | {CONF_NAME: name})
return SwitchTemplate(hass, validated_config, None)

View File

@ -454,6 +454,37 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
),
TuyaSensorEntityDescription(
key=DPCode.VA_TEMPERATURE,
translation_key="temperature",
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
),
TuyaSensorEntityDescription(
key=DPCode.TEMP_CURRENT,
translation_key="temperature",
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
),
TuyaSensorEntityDescription(
key=DPCode.VA_HUMIDITY,
translation_key="humidity",
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
TuyaSensorEntityDescription(
key=DPCode.HUMIDITY_VALUE,
translation_key="humidity",
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
TuyaSensorEntityDescription(
key=DPCode.BRIGHT_VALUE,
translation_key="illuminance",
device_class=SensorDeviceClass.ILLUMINANCE,
state_class=SensorStateClass.MEASUREMENT,
),
*BATTERY_SENSORS,
),
# Luminance Sensor
# https://developer.tuya.com/en/docs/iot/categoryldcg?id=Kaiuz3n7u69l8

View File

@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aiowebdav2"],
"quality_scale": "bronze",
"requirements": ["aiowebdav2==0.4.2"]
"requirements": ["aiowebdav2==0.4.4"]
}

View File

@ -21,7 +21,7 @@ type AsyncWebSocketCommandHandler = Callable[
DOMAIN: Final = "websocket_api"
URL: Final = "/api/websocket"
PENDING_MSG_PEAK: Final = 1024
PENDING_MSG_PEAK_TIME: Final = 5
PENDING_MSG_PEAK_TIME: Final = 10
# Maximum number of messages that can be pending at any given time.
# This is effectively the upper limit of the number of entities
# that can fire state changes within ~1 second.

View File

@ -2,13 +2,13 @@
"title": "Workday",
"config": {
"abort": {
"already_configured": "Workday has already been setup with chosen configuration"
"already_configured": "Workday has already been set up with chosen configuration"
},
"step": {
"user": {
"data": {
"name": "[%key:common::config_flow::data::name%]",
"country": "Country"
"country": "[%key:common::config_flow::data::country%]"
}
},
"options": {
@ -18,7 +18,7 @@
"days_offset": "Offset",
"workdays": "Days to include",
"add_holidays": "Add holidays",
"remove_holidays": "Remove Holidays",
"remove_holidays": "Remove holidays",
"province": "Subdivision of country",
"language": "Language for named holidays",
"category": "Additional category as holiday"
@ -116,14 +116,14 @@
},
"issues": {
"bad_country": {
"title": "Configured Country for {title} does not exist",
"title": "Configured country for {title} does not exist",
"fix_flow": {
"step": {
"country": {
"title": "Select country for {title}",
"description": "Select a country to use for your Workday sensor.",
"data": {
"country": "[%key:component::workday::config::step::user::data::country%]"
"country": "[%key:common::config_flow::data::country%]"
}
},
"province": {
@ -133,7 +133,7 @@
"province": "[%key:component::workday::config::step::options::data::province%]"
},
"data_description": {
"province": "State, Territory, Province, Region of Country"
"province": "[%key:component::workday::config::step::options::data_description::province%]"
}
}
}
@ -150,7 +150,7 @@
"province": "[%key:component::workday::config::step::options::data::province%]"
},
"data_description": {
"province": "[%key:component::workday::issues::bad_country::fix_flow::step::province::data_description::province%]"
"province": "[%key:component::workday::config::step::options::data_description::province%]"
}
}
}
@ -217,7 +217,7 @@
"services": {
"check_date": {
"name": "Check date",
"description": "Check if date is workday.",
"description": "Checks if a given date is a workday.",
"fields": {
"check_date": {
"name": "Date",

View File

@ -145,8 +145,6 @@ def _async_get_instance(hass: HomeAssistant) -> HaAsyncZeroconf:
if DOMAIN in hass.data:
return cast(HaAsyncZeroconf, hass.data[DOMAIN])
logging.getLogger("zeroconf").setLevel(logging.NOTSET)
zeroconf = HaZeroconf(**_async_get_zc_args(hass))
aio_zc = HaAsyncZeroconf(zc=zeroconf)

View File

@ -25,7 +25,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2025
MINOR_VERSION: Final = 4
PATCH_VERSION: Final = "0.dev0"
PATCH_VERSION: Final = "0b9"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 0)

View File

@ -759,17 +759,28 @@
"config_flow": true,
"iot_class": "local_push"
},
"bosch_alarm": {
"name": "Bosch Alarm",
"integration_type": "device",
"config_flow": true,
"iot_class": "local_push"
},
"bosch_shc": {
"name": "Bosch SHC",
"integration_type": "hub",
"config_flow": true,
"iot_class": "local_push"
"bosch": {
"name": "Bosch",
"integrations": {
"bosch_alarm": {
"integration_type": "device",
"config_flow": true,
"iot_class": "local_push",
"name": "Bosch Alarm"
},
"bosch_shc": {
"integration_type": "hub",
"config_flow": true,
"iot_class": "local_push",
"name": "Bosch SHC"
},
"home_connect": {
"integration_type": "hub",
"config_flow": true,
"iot_class": "cloud_push",
"name": "Home Connect"
}
}
},
"brandt": {
"name": "Brandt Smart Control",
@ -2639,13 +2650,6 @@
"config_flow": true,
"iot_class": "local_polling"
},
"home_connect": {
"name": "Home Connect",
"integration_type": "hub",
"config_flow": true,
"iot_class": "cloud_push",
"single_config_entry": true
},
"home_plus_control": {
"name": "Legrand Home+ Control",
"integration_type": "virtual",

View File

@ -1311,7 +1311,7 @@ class _QueuedScriptRun(_ScriptRun):
lock_acquired = False
async def async_run(self) -> None:
async def async_run(self) -> ScriptRunResult | None:
"""Run script."""
# Wait for previous run, if any, to finish by attempting to acquire the script's
# shared lock. At the same time monitor if we've been told to stop.
@ -1325,7 +1325,7 @@ class _QueuedScriptRun(_ScriptRun):
self.lock_acquired = True
# We've acquired the lock so we can go ahead and start the run.
await super().async_run()
return await super().async_run()
def _finish(self) -> None:
if self.lock_acquired:

View File

@ -38,8 +38,8 @@ habluetooth==3.37.0
hass-nabucasa==0.94.0
hassil==2.2.3
home-assistant-bluetooth==1.13.1
home-assistant-frontend==20250326.0
home-assistant-intents==2025.3.24
home-assistant-frontend==20250328.0
home-assistant-intents==2025.3.28
httpx==0.28.1
ifaddr==0.2.0
Jinja2==3.1.6

View File

@ -47,6 +47,7 @@
"access_token": "Access token",
"api_key": "API key",
"api_token": "API token",
"country": "Country",
"device": "Device",
"elevation": "Elevation",
"email": "Email",

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2025.4.0.dev0"
version = "2025.4.0b9"
license = "Apache-2.0"
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
description = "Open-source home automation platform running on Python 3."

16
requirements_all.txt generated
View File

@ -225,7 +225,7 @@ aiodiscover==2.6.1
aiodns==3.2.0
# homeassistant.components.duke_energy
aiodukeenergy==0.2.2
aiodukeenergy==0.3.0
# homeassistant.components.eafm
aioeafm==0.1.2
@ -422,7 +422,7 @@ aiowaqi==3.1.0
aiowatttime==0.1.1
# homeassistant.components.webdav
aiowebdav2==0.4.2
aiowebdav2==0.4.4
# homeassistant.components.webostv
aiowebostv==0.7.3
@ -758,7 +758,7 @@ debugpy==1.8.13
# decora==0.6
# homeassistant.components.ecovacs
deebot-client==12.3.1
deebot-client==12.4.0
# homeassistant.components.ihc
# homeassistant.components.namecheapdns
@ -1157,10 +1157,10 @@ hole==0.8.0
holidays==0.69
# homeassistant.components.frontend
home-assistant-frontend==20250326.0
home-assistant-frontend==20250328.0
# homeassistant.components.conversation
home-assistant-intents==2025.3.24
home-assistant-intents==2025.3.28
# homeassistant.components.homematicip_cloud
homematicip==1.1.7
@ -1453,7 +1453,7 @@ mozart-api==4.1.1.116.4
mullvad-api==1.0.0
# homeassistant.components.music_assistant
music-assistant-client==1.1.1
music-assistant-client==1.2.0
# homeassistant.components.tts
mutagen==1.47.0
@ -2430,7 +2430,7 @@ python-juicenet==1.1.0
python-kasa[speedups]==0.10.2
# homeassistant.components.linkplay
python-linkplay==0.2.1
python-linkplay==0.2.2
# homeassistant.components.lirc
# python-lirc==1.2.3
@ -2476,7 +2476,7 @@ python-roborock==2.16.1
python-smarttub==0.0.39
# homeassistant.components.snoo
python-snoo==0.6.4
python-snoo==0.6.5
# homeassistant.components.songpal
python-songpal==0.16.2

View File

@ -213,7 +213,7 @@ aiodiscover==2.6.1
aiodns==3.2.0
# homeassistant.components.duke_energy
aiodukeenergy==0.2.2
aiodukeenergy==0.3.0
# homeassistant.components.eafm
aioeafm==0.1.2
@ -404,7 +404,7 @@ aiowaqi==3.1.0
aiowatttime==0.1.1
# homeassistant.components.webdav
aiowebdav2==0.4.2
aiowebdav2==0.4.4
# homeassistant.components.webostv
aiowebostv==0.7.3
@ -649,7 +649,7 @@ dbus-fast==2.43.0
debugpy==1.8.13
# homeassistant.components.ecovacs
deebot-client==12.3.1
deebot-client==12.4.0
# homeassistant.components.ihc
# homeassistant.components.namecheapdns
@ -984,10 +984,10 @@ hole==0.8.0
holidays==0.69
# homeassistant.components.frontend
home-assistant-frontend==20250326.0
home-assistant-frontend==20250328.0
# homeassistant.components.conversation
home-assistant-intents==2025.3.24
home-assistant-intents==2025.3.28
# homeassistant.components.homematicip_cloud
homematicip==1.1.7
@ -1223,7 +1223,7 @@ mozart-api==4.1.1.116.4
mullvad-api==1.0.0
# homeassistant.components.music_assistant
music-assistant-client==1.1.1
music-assistant-client==1.2.0
# homeassistant.components.tts
mutagen==1.47.0
@ -1967,7 +1967,7 @@ python-juicenet==1.1.0
python-kasa[speedups]==0.10.2
# homeassistant.components.linkplay
python-linkplay==0.2.1
python-linkplay==0.2.2
# homeassistant.components.matter
python-matter-server==7.0.0
@ -2007,7 +2007,7 @@ python-roborock==2.16.1
python-smarttub==0.0.39
# homeassistant.components.snoo
python-snoo==0.6.4
python-snoo==0.6.5
# homeassistant.components.songpal
python-songpal==0.16.2

View File

@ -25,7 +25,7 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.6.10,source=/uv,target=/bin/uv \
-c /usr/src/homeassistant/homeassistant/package_constraints.txt \
-r /usr/src/homeassistant/requirements.txt \
stdlib-list==0.10.0 pipdeptree==2.25.1 tqdm==4.67.1 ruff==0.11.0 \
PyTurboJPEG==1.7.5 go2rtc-client==0.1.2 ha-ffmpeg==3.2.2 hassil==2.2.3 home-assistant-intents==2025.3.24 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2
PyTurboJPEG==1.7.5 go2rtc-client==0.1.2 ha-ffmpeg==3.2.2 hassil==2.2.3 home-assistant-intents==2025.3.28 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2
LABEL "name"="hassfest"
LABEL "maintainer"="Home Assistant <hello@home-assistant.io>"

View File

@ -22,6 +22,7 @@ from homeassistant.components.assist_satellite import (
AssistSatelliteAnnouncement,
SatelliteBusyError,
)
from homeassistant.components.assist_satellite.const import PREANNOUNCE_URL
from homeassistant.components.assist_satellite.entity import AssistSatelliteState
from homeassistant.components.media_source import PlayMedia
from homeassistant.config_entries import ConfigEntry
@ -185,7 +186,7 @@ async def test_new_pipeline_cancels_pipeline(
("service_data", "expected_params"),
[
(
{"message": "Hello"},
{"message": "Hello", "preannounce_media_id": None},
AssistSatelliteAnnouncement(
message="Hello",
media_id="http://10.10.10.10:8123/api/tts_proxy/test-token",
@ -198,6 +199,7 @@ async def test_new_pipeline_cancels_pipeline(
{
"message": "Hello",
"media_id": "media-source://given",
"preannounce_media_id": None,
},
AssistSatelliteAnnouncement(
message="Hello",
@ -208,7 +210,7 @@ async def test_new_pipeline_cancels_pipeline(
),
),
(
{"media_id": "http://example.com/bla.mp3"},
{"media_id": "http://example.com/bla.mp3", "preannounce_media_id": None},
AssistSatelliteAnnouncement(
message="",
media_id="http://example.com/bla.mp3",
@ -368,6 +370,24 @@ async def test_announce_cancels_pipeline(
mock_async_announce.assert_called_once()
async def test_announce_default_preannounce(
hass: HomeAssistant, init_components: ConfigEntry, entity: MockAssistSatellite
) -> None:
"""Test announcing on a device with the default preannouncement sound."""
async def async_announce(announcement):
assert announcement.preannounce_media_id.endswith(PREANNOUNCE_URL)
with patch.object(entity, "async_announce", new=async_announce):
await hass.services.async_call(
"assist_satellite",
"announce",
{"media_id": "test-media-id"},
target={"entity_id": "assist_satellite.test_entity"},
blocking=True,
)
async def test_context_refresh(
hass: HomeAssistant, init_components: ConfigEntry, entity: MockAssistSatellite
) -> None:
@ -521,6 +541,7 @@ async def test_vad_sensitivity_entity_not_found(
{
"start_message": "Hello",
"extra_system_prompt": "Better system prompt",
"preannounce_media_id": None,
},
(
"mock-conversation-id",
@ -538,6 +559,7 @@ async def test_vad_sensitivity_entity_not_found(
{
"start_message": "Hello",
"start_media_id": "media-source://given",
"preannounce_media_id": None,
},
(
"mock-conversation-id",
@ -552,7 +574,10 @@ async def test_vad_sensitivity_entity_not_found(
),
),
(
{"start_media_id": "http://example.com/given.mp3"},
{
"start_media_id": "http://example.com/given.mp3",
"preannounce_media_id": None,
},
(
"mock-conversation-id",
None,
@ -657,6 +682,32 @@ async def test_start_conversation_reject_builtin_agent(
)
async def test_start_conversation_default_preannounce(
hass: HomeAssistant, init_components: ConfigEntry, entity: MockAssistSatellite
) -> None:
"""Test starting a conversation on a device with the default preannouncement sound."""
async def async_start_conversation(start_announcement):
assert PREANNOUNCE_URL in start_announcement.preannounce_media_id
await async_update_pipeline(
hass,
async_get_pipeline(hass),
conversation_engine="conversation.some_llm",
)
with (
patch.object(entity, "async_start_conversation", new=async_start_conversation),
):
await hass.services.async_call(
"assist_satellite",
"start_conversation",
{"start_media_id": "test-media-id"},
target={"entity_id": "assist_satellite.test_entity"},
blocking=True,
)
async def test_wake_word_start_keeps_responding(
hass: HomeAssistant, init_components: ConfigEntry, entity: MockAssistSatellite
) -> None:

View File

@ -445,6 +445,7 @@ async def test_connection_test(
assert len(entity.announcements) == 1
assert entity.announcements[0].message == ""
assert entity.announcements[0].preannounce_media_id is None
announcement_media_id = entity.announcements[0].media_id
hass_url = "http://10.10.10.10:8123"
assert announcement_media_id.startswith(

View File

@ -127,7 +127,7 @@ async def test_awair_gen1_sensors(
assert_expected_properties(
hass,
entity_registry,
"sensor.living_room_vocs",
"sensor.living_room_volatile_organic_compounds_parts",
f"{AWAIR_UUID}_{SENSOR_TYPES_MAP[API_VOC].unique_id_tag}",
"366",
{

View File

@ -5,9 +5,9 @@ from io import StringIO
from typing import Any
from unittest.mock import ANY, Mock, PropertyMock, patch
from aiohttp import ClientError
from aiohttp import ClientError, ClientResponseError
from hass_nabucasa import CloudError
from hass_nabucasa.api import CloudApiNonRetryableError
from hass_nabucasa.api import CloudApiError, CloudApiNonRetryableError
from hass_nabucasa.files import FilesError, StorageType
import pytest
@ -547,6 +547,120 @@ async def test_agents_upload_not_protected(
assert stored_backup["failed_agent_ids"] == ["cloud.cloud"]
@pytest.mark.usefixtures("cloud_logged_in", "mock_list_files")
async def test_agents_upload_not_subscribed(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
hass_storage: dict[str, Any],
cloud: Mock,
) -> None:
"""Test upload backup when cloud user is not subscribed."""
cloud.subscription_expired = True
client = await hass_client()
backup_data = "test"
backup_id = "test-backup"
test_backup = AgentBackup(
addons=[AddonInfo(name="Test", slug="test", version="1.0.0")],
backup_id=backup_id,
database_included=True,
date="1970-01-01T00:00:00.000Z",
extra_metadata={},
folders=[Folder.MEDIA, Folder.SHARE],
homeassistant_included=True,
homeassistant_version="2024.12.0",
name="Test",
protected=True,
size=len(backup_data),
)
with (
patch(
"homeassistant.components.backup.manager.BackupManager.async_get_backup",
) as fetch_backup,
patch(
"homeassistant.components.backup.manager.read_backup",
return_value=test_backup,
),
patch("pathlib.Path.open") as mocked_open,
):
mocked_open.return_value.read = Mock(side_effect=[backup_data.encode(), b""])
fetch_backup.return_value = test_backup
resp = await client.post(
"/api/backup/upload?agent_id=cloud.cloud",
data={"file": StringIO(backup_data)},
)
await hass.async_block_till_done()
assert resp.status == 201
assert cloud.files.upload.call_count == 0
store_backups = hass_storage[BACKUP_DOMAIN]["data"]["backups"]
assert len(store_backups) == 1
stored_backup = store_backups[0]
assert stored_backup["backup_id"] == backup_id
assert stored_backup["failed_agent_ids"] == ["cloud.cloud"]
@pytest.mark.usefixtures("cloud_logged_in", "mock_list_files")
async def test_agents_upload_not_subscribed_midway(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
hass_storage: dict[str, Any],
cloud: Mock,
) -> None:
"""Test upload backup when cloud subscription expires during the call."""
client = await hass_client()
backup_data = "test"
backup_id = "test-backup"
test_backup = AgentBackup(
addons=[AddonInfo(name="Test", slug="test", version="1.0.0")],
backup_id=backup_id,
database_included=True,
date="1970-01-01T00:00:00.000Z",
extra_metadata={},
folders=[Folder.MEDIA, Folder.SHARE],
homeassistant_included=True,
homeassistant_version="2024.12.0",
name="Test",
protected=True,
size=len(backup_data),
)
async def mock_upload(*args: Any, **kwargs: Any) -> None:
"""Mock file upload."""
cloud.subscription_expired = True
raise CloudApiError(
"Boom!", orig_exc=ClientResponseError(Mock(), Mock(), status=403)
)
cloud.files.upload.side_effect = mock_upload
with (
patch(
"homeassistant.components.backup.manager.BackupManager.async_get_backup",
) as fetch_backup,
patch(
"homeassistant.components.backup.manager.read_backup",
return_value=test_backup,
),
patch("pathlib.Path.open") as mocked_open,
):
mocked_open.return_value.read = Mock(side_effect=[backup_data.encode(), b""])
fetch_backup.return_value = test_backup
resp = await client.post(
"/api/backup/upload?agent_id=cloud.cloud",
data={"file": StringIO(backup_data)},
)
await hass.async_block_till_done()
assert resp.status == 201
assert cloud.files.upload.call_count == 1
store_backups = hass_storage[BACKUP_DOMAIN]["data"]["backups"]
assert len(store_backups) == 1
stored_backup = store_backups[0]
assert stored_backup["backup_id"] == backup_id
assert stored_backup["failed_agent_ids"] == ["cloud.cloud"]
@pytest.mark.usefixtures("cloud_logged_in", "mock_list_files")
async def test_agents_upload_wrong_size(
hass: HomeAssistant,

View File

@ -61,8 +61,8 @@ def mock_api() -> Generator[AsyncMock]:
):
api = mock_api.return_value
api.authenticate.return_value = {
"email": "TEST@EXAMPLE.COM",
"cdp_internal_user_id": "test-username",
"loginEmailAddress": "TEST@EXAMPLE.COM",
"internalUserID": "test-username",
}
api.get_meters.return_value = {}
yield api

View File

@ -1249,7 +1249,11 @@ async def test_announce_message(
await hass.services.async_call(
assist_satellite.DOMAIN,
"announce",
{"entity_id": satellite.entity_id, "message": "test-text"},
{
"entity_id": satellite.entity_id,
"message": "test-text",
"preannounce_media_id": None,
},
blocking=True,
)
await done.wait()
@ -1338,6 +1342,7 @@ async def test_announce_media_id(
{
"entity_id": satellite.entity_id,
"media_id": "https://www.home-assistant.io/resolved.mp3",
"preannounce_media_id": None,
},
blocking=True,
)
@ -1545,7 +1550,11 @@ async def test_start_conversation_message(
await hass.services.async_call(
assist_satellite.DOMAIN,
"start_conversation",
{"entity_id": satellite.entity_id, "start_message": "test-text"},
{
"entity_id": satellite.entity_id,
"start_message": "test-text",
"preannounce_media_id": None,
},
blocking=True,
)
await done.wait()
@ -1653,6 +1662,7 @@ async def test_start_conversation_media_id(
{
"entity_id": satellite.entity_id,
"start_media_id": "https://www.home-assistant.io/resolved.mp3",
"preannounce_media_id": None,
},
blocking=True,
)

View File

@ -260,6 +260,76 @@ async def test_entities_removed_after_reload(
assert len(hass_storage[storage_key]["data"]["binary_sensor"]) == 1
async def test_entities_for_entire_platform_removed(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
mock_client: APIClient,
hass_storage: dict[str, Any],
mock_esphome_device: Callable[
[APIClient, list[EntityInfo], list[UserService], list[EntityState]],
Awaitable[MockESPHomeDevice],
],
) -> None:
"""Test removing all entities for a specific platform when static info changes."""
entity_info = [
BinarySensorInfo(
object_id="mybinary_sensor_to_be_removed",
key=1,
name="my binary_sensor to be removed",
unique_id="mybinary_sensor_to_be_removed",
),
]
states = [
BinarySensorState(key=1, state=True, missing_state=False),
]
user_service = []
mock_device = await mock_esphome_device(
mock_client=mock_client,
entity_info=entity_info,
user_service=user_service,
states=states,
)
entry = mock_device.entry
entry_id = entry.entry_id
storage_key = f"esphome.{entry_id}"
state = hass.states.get("binary_sensor.test_mybinary_sensor_to_be_removed")
assert state is not None
assert state.state == STATE_ON
await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()
assert len(hass_storage[storage_key]["data"]["binary_sensor"]) == 1
state = hass.states.get("binary_sensor.test_mybinary_sensor_to_be_removed")
assert state is not None
reg_entry = entity_registry.async_get(
"binary_sensor.test_mybinary_sensor_to_be_removed"
)
assert reg_entry is not None
assert state.attributes[ATTR_RESTORED] is True
entity_info = []
states = []
mock_device = await mock_esphome_device(
mock_client=mock_client,
entity_info=entity_info,
user_service=user_service,
states=states,
entry=entry,
)
assert mock_device.entry.entry_id == entry_id
state = hass.states.get("binary_sensor.test_mybinary_sensor_to_be_removed")
assert state is None
reg_entry = entity_registry.async_get(
"binary_sensor.test_mybinary_sensor_to_be_removed"
)
assert reg_entry is None
await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()
assert len(hass_storage[storage_key]["data"]["binary_sensor"]) == 0
async def test_entity_info_object_ids(
hass: HomeAssistant,
mock_client: APIClient,

View File

@ -4,6 +4,7 @@ from aioesphomeapi import APIClient, Event, EventInfo
import pytest
from homeassistant.components.event import EventDeviceClass
from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
@ -11,9 +12,9 @@ from homeassistant.core import HomeAssistant
async def test_generic_event_entity(
hass: HomeAssistant,
mock_client: APIClient,
mock_generic_device_entry,
mock_esphome_device,
) -> None:
"""Test a generic event entity."""
"""Test a generic event entity and its availability behavior."""
entity_info = [
EventInfo(
object_id="myevent",
@ -26,13 +27,31 @@ async def test_generic_event_entity(
]
states = [Event(key=1, event_type="type1")]
user_service = []
await mock_generic_device_entry(
device = await mock_esphome_device(
mock_client=mock_client,
entity_info=entity_info,
user_service=user_service,
states=states,
)
await hass.async_block_till_done()
# Test initial state
state = hass.states.get("event.test_myevent")
assert state is not None
assert state.state == "2024-04-24T00:00:00.000+00:00"
assert state.attributes["event_type"] == "type1"
# Test device becomes unavailable
await device.mock_disconnect(True)
await hass.async_block_till_done()
state = hass.states.get("event.test_myevent")
assert state.state == STATE_UNAVAILABLE
# Test device becomes available again
await device.mock_connect()
await hass.async_block_till_done()
# Event entity should be available immediately without waiting for data
state = hass.states.get("event.test_myevent")
assert state.state == "2024-04-24T00:00:00.000+00:00"
assert state.attributes["event_type"] == "type1"

View File

@ -86,26 +86,28 @@ def stub_reconnect():
)
async def test_update_entity(
hass: HomeAssistant,
stub_reconnect,
mock_config_entry,
mock_device_info,
mock_dashboard: dict[str, Any],
devices_payload,
expected_state,
expected_attributes,
devices_payload: list[dict[str, Any]],
expected_state: str,
expected_attributes: dict[str, Any],
mock_client: APIClient,
mock_esphome_device: Callable[
[APIClient, list[EntityInfo], list[UserService], list[EntityState]],
Awaitable[MockESPHomeDevice],
],
) -> None:
"""Test ESPHome update entity."""
mock_dashboard["configured"] = devices_payload
await async_get_dashboard(hass).async_refresh()
with patch(
"homeassistant.components.esphome.update.DomainData.get_entry_data",
return_value=Mock(available=True, device_info=mock_device_info, info={}),
):
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
await mock_esphome_device(
mock_client=mock_client,
entity_info=[],
user_service=[],
states=[],
)
state = hass.states.get("update.none_firmware")
state = hass.states.get("update.test_firmware")
assert state is not None
assert state.state == expected_state
for key, expected_value in expected_attributes.items():
@ -130,7 +132,7 @@ async def test_update_entity(
await hass.services.async_call(
"update",
"install",
{"entity_id": "update.none_firmware"},
{"entity_id": "update.test_firmware"},
blocking=True,
)
@ -155,7 +157,7 @@ async def test_update_entity(
await hass.services.async_call(
"update",
"install",
{"entity_id": "update.none_firmware"},
{"entity_id": "update.test_firmware"},
blocking=True,
)
@ -177,7 +179,7 @@ async def test_update_entity(
await hass.services.async_call(
"update",
"install",
{"entity_id": "update.none_firmware"},
{"entity_id": "update.test_firmware"},
blocking=True,
)
@ -274,28 +276,30 @@ async def test_update_device_state_for_availability(
async def test_update_entity_dashboard_not_available_startup(
hass: HomeAssistant,
stub_reconnect,
mock_config_entry,
mock_device_info,
mock_client: APIClient,
mock_esphome_device: Callable[
[APIClient, list[EntityInfo], list[UserService], list[EntityState]],
Awaitable[MockESPHomeDevice],
],
mock_dashboard: dict[str, Any],
) -> None:
"""Test ESPHome update entity when dashboard is not available at startup."""
with (
patch(
"homeassistant.components.esphome.update.DomainData.get_entry_data",
return_value=Mock(available=True, device_info=mock_device_info, info={}),
),
patch(
"esphome_dashboard_api.ESPHomeDashboardAPI.get_devices",
side_effect=TimeoutError,
),
):
await async_get_dashboard(hass).async_refresh()
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
await mock_esphome_device(
mock_client=mock_client,
entity_info=[],
user_service=[],
states=[],
)
# We have a dashboard but it is not available
state = hass.states.get("update.none_firmware")
state = hass.states.get("update.test_firmware")
assert state is None
mock_dashboard["configured"] = [
@ -308,7 +312,7 @@ async def test_update_entity_dashboard_not_available_startup(
await async_get_dashboard(hass).async_refresh()
await hass.async_block_till_done()
state = hass.states.get("update.none_firmware")
state = hass.states.get("update.test_firmware")
assert state.state == STATE_ON
expected_attributes = {
"latest_version": "2023.2.0-dev",

View File

@ -16,10 +16,10 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'switch',
'friendly_name': 'fake-device-1 Quiet',
'friendly_name': 'fake-device-1 Quiet mode',
}),
'context': <ANY>,
'entity_id': 'switch.fake_device_1_quiet',
'entity_id': 'switch.fake_device_1_quiet_mode',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
@ -40,10 +40,10 @@
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'switch',
'friendly_name': 'fake-device-1 XFan',
'friendly_name': 'fake-device-1 Xtra fan',
}),
'context': <ANY>,
'entity_id': 'switch.fake_device_1_xfan',
'entity_id': 'switch.fake_device_1_xtra_fan',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
@ -109,7 +109,7 @@
'disabled_by': None,
'domain': 'switch',
'entity_category': None,
'entity_id': 'switch.fake_device_1_quiet',
'entity_id': 'switch.fake_device_1_quiet_mode',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
@ -121,7 +121,7 @@
}),
'original_device_class': <SwitchDeviceClass.SWITCH: 'switch'>,
'original_icon': None,
'original_name': 'Quiet',
'original_name': 'Quiet mode',
'platform': 'gree',
'previous_unique_id': None,
'supported_features': 0,
@ -173,7 +173,7 @@
'disabled_by': None,
'domain': 'switch',
'entity_category': None,
'entity_id': 'switch.fake_device_1_xfan',
'entity_id': 'switch.fake_device_1_xtra_fan',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
@ -185,7 +185,7 @@
}),
'original_device_class': <SwitchDeviceClass.SWITCH: 'switch'>,
'original_icon': None,
'original_name': 'XFan',
'original_name': 'Xtra fan',
'platform': 'gree',
'previous_unique_id': None,
'supported_features': 0,

View File

@ -22,11 +22,11 @@ from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
ENTITY_ID_LIGHT_PANEL = f"{SWITCH_DOMAIN}.fake_device_1_panel_light"
ENTITY_ID_PANEL_LIGHT = f"{SWITCH_DOMAIN}.fake_device_1_panel_light"
ENTITY_ID_HEALTH_MODE = f"{SWITCH_DOMAIN}.fake_device_1_health_mode"
ENTITY_ID_QUIET = f"{SWITCH_DOMAIN}.fake_device_1_quiet"
ENTITY_ID_QUIET_MODE = f"{SWITCH_DOMAIN}.fake_device_1_quiet_mode"
ENTITY_ID_FRESH_AIR = f"{SWITCH_DOMAIN}.fake_device_1_fresh_air"
ENTITY_ID_XFAN = f"{SWITCH_DOMAIN}.fake_device_1_xfan"
ENTITY_ID_XTRA_FAN = f"{SWITCH_DOMAIN}.fake_device_1_xtra_fan"
async def async_setup_gree(hass: HomeAssistant) -> MockConfigEntry:
@ -54,11 +54,11 @@ async def test_registry_settings(
@pytest.mark.parametrize(
"entity",
[
ENTITY_ID_LIGHT_PANEL,
ENTITY_ID_PANEL_LIGHT,
ENTITY_ID_HEALTH_MODE,
ENTITY_ID_QUIET,
ENTITY_ID_QUIET_MODE,
ENTITY_ID_FRESH_AIR,
ENTITY_ID_XFAN,
ENTITY_ID_XTRA_FAN,
],
)
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
@ -81,11 +81,11 @@ async def test_send_switch_on(hass: HomeAssistant, entity: str) -> None:
@pytest.mark.parametrize(
"entity",
[
ENTITY_ID_LIGHT_PANEL,
ENTITY_ID_PANEL_LIGHT,
ENTITY_ID_HEALTH_MODE,
ENTITY_ID_QUIET,
ENTITY_ID_QUIET_MODE,
ENTITY_ID_FRESH_AIR,
ENTITY_ID_XFAN,
ENTITY_ID_XTRA_FAN,
],
)
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
@ -112,11 +112,11 @@ async def test_send_switch_on_device_timeout(
@pytest.mark.parametrize(
"entity",
[
ENTITY_ID_LIGHT_PANEL,
ENTITY_ID_PANEL_LIGHT,
ENTITY_ID_HEALTH_MODE,
ENTITY_ID_QUIET,
ENTITY_ID_QUIET_MODE,
ENTITY_ID_FRESH_AIR,
ENTITY_ID_XFAN,
ENTITY_ID_XTRA_FAN,
],
)
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
@ -139,11 +139,11 @@ async def test_send_switch_off(hass: HomeAssistant, entity: str) -> None:
@pytest.mark.parametrize(
"entity",
[
ENTITY_ID_LIGHT_PANEL,
ENTITY_ID_PANEL_LIGHT,
ENTITY_ID_HEALTH_MODE,
ENTITY_ID_QUIET,
ENTITY_ID_QUIET_MODE,
ENTITY_ID_FRESH_AIR,
ENTITY_ID_XFAN,
ENTITY_ID_XTRA_FAN,
],
)
@pytest.mark.usefixtures("entity_registry_enabled_by_default")

View File

@ -1,6 +1,7 @@
"""Tests for home_connect binary_sensor entities."""
from collections.abc import Awaitable, Callable
from http import HTTPStatus
from unittest.mock import AsyncMock, MagicMock
from aiohomeconnect.model import (
@ -39,6 +40,7 @@ import homeassistant.helpers.issue_registry as ir
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
from tests.typing import ClientSessionGenerator
@pytest.fixture
@ -165,6 +167,7 @@ async def test_connected_devices(
assert len(new_entity_entries) > len(entity_entries)
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
@pytest.mark.parametrize("appliance", ["Washer"], indirect=True)
async def test_binary_sensors_entity_availability(
hass: HomeAssistant,
@ -219,6 +222,7 @@ async def test_binary_sensors_entity_availability(
assert state.state != STATE_UNAVAILABLE
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
@pytest.mark.parametrize("appliance", ["Washer"], indirect=True)
@pytest.mark.parametrize(
("value", "expected"),
@ -402,7 +406,7 @@ async def test_connected_sensor_functionality(
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_create_issue(
async def test_create_door_binary_sensor_deprecation_issue(
hass: HomeAssistant,
config_entry: MockConfigEntry,
integration_setup: Callable[[MagicMock], Awaitable[bool]],
@ -410,7 +414,7 @@ async def test_create_issue(
client: MagicMock,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test we create an issue when an automation or script is using a deprecated entity."""
"""Test that we create an issue when an automation or script is using a door binary sensor entity."""
entity_id = "binary_sensor.washer_door"
issue_id = f"deprecated_binary_common_door_sensor_{entity_id}"
@ -464,3 +468,76 @@ async def test_create_issue(
# Assert the issue is no longer present
assert not issue_registry.async_get_issue(DOMAIN, issue_id)
assert len(issue_registry.issues) == 0
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_door_binary_sensor_deprecation_issue_fix(
hass: HomeAssistant,
config_entry: MockConfigEntry,
integration_setup: Callable[[MagicMock], Awaitable[bool]],
setup_credentials: None,
client: MagicMock,
issue_registry: ir.IssueRegistry,
hass_client: ClientSessionGenerator,
) -> None:
"""Test that we create an issue when an automation or script is using a door binary sensor entity."""
entity_id = "binary_sensor.washer_door"
issue_id = f"deprecated_binary_common_door_sensor_{entity_id}"
assert await async_setup_component(
hass,
automation.DOMAIN,
{
automation.DOMAIN: {
"alias": "test",
"trigger": {"platform": "state", "entity_id": entity_id},
"action": {
"action": "automation.turn_on",
"target": {
"entity_id": "automation.test",
},
},
}
},
)
assert await async_setup_component(
hass,
script.DOMAIN,
{
script.DOMAIN: {
"test": {
"sequence": [
{
"condition": "state",
"entity_id": entity_id,
"state": "on",
},
],
}
}
},
)
assert config_entry.state == ConfigEntryState.NOT_LOADED
assert await integration_setup(client)
assert config_entry.state == ConfigEntryState.LOADED
assert automations_with_entity(hass, entity_id)[0] == "automation.test"
assert scripts_with_entity(hass, entity_id)[0] == "script.test"
assert len(issue_registry.issues) == 1
issue = issue_registry.async_get_issue(DOMAIN, issue_id)
assert issue
_client = await hass_client()
resp = await _client.post(
"/api/repairs/issues/fix",
json={"handler": DOMAIN, "issue_id": issue.issue_id},
)
assert resp.status == HTTPStatus.OK
flow_id = (await resp.json())["flow_id"]
resp = await _client.post(f"/api/repairs/issues/fix/{flow_id}")
# Assert the issue is no longer present
assert not issue_registry.async_get_issue(DOMAIN, issue_id)
assert len(issue_registry.issues) == 0

View File

@ -1,6 +1,7 @@
"""Tests for home_connect sensor entities."""
from collections.abc import Awaitable, Callable
from http import HTTPStatus
from typing import Any
from unittest.mock import AsyncMock, MagicMock
@ -59,6 +60,7 @@ from homeassistant.helpers import (
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
from tests.typing import ClientSessionGenerator
@pytest.fixture
@ -209,6 +211,7 @@ async def test_connected_devices(
assert len(new_entity_entries) > len(entity_entries)
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
@pytest.mark.parametrize("appliance", ["Dishwasher"], indirect=True)
async def test_switch_entity_availability(
hass: HomeAssistant,
@ -320,6 +323,7 @@ async def test_switch_functionality(
assert hass.states.is_state(entity_id, state)
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
@pytest.mark.parametrize(
("entity_id", "program_key", "initial_state", "appliance"),
[
@ -397,6 +401,7 @@ async def test_program_switch_functionality(
client.stop_program.assert_awaited_once_with(appliance.ha_id)
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
@pytest.mark.parametrize(
(
"entity_id",
@ -801,18 +806,24 @@ async def test_power_switch_service_validation_errors(
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_create_issue(
@pytest.mark.parametrize(
"service",
[SERVICE_TURN_ON, SERVICE_TURN_OFF],
)
async def test_create_program_switch_deprecation_issue(
hass: HomeAssistant,
appliance: HomeAppliance,
service: str,
config_entry: MockConfigEntry,
integration_setup: Callable[[MagicMock], Awaitable[bool]],
setup_credentials: None,
client: MagicMock,
issue_registry: ir.IssueRegistry,
) -> None:
"""Test we create an issue when an automation or script is using a deprecated entity."""
"""Test that we create an issue when an automation or script is using a program switch entity or the entity is used by the user."""
entity_id = "switch.washer_program_mix"
issue_id = f"deprecated_program_switch_{entity_id}"
automation_script_issue_id = f"deprecated_program_switch_{entity_id}"
action_handler_issue_id = f"deprecated_program_switch_{entity_id}"
assert await async_setup_component(
hass,
@ -851,17 +862,118 @@ async def test_create_issue(
assert await integration_setup(client)
assert config_entry.state == ConfigEntryState.LOADED
await hass.services.async_call(
SWITCH_DOMAIN,
service,
{
ATTR_ENTITY_ID: entity_id,
},
blocking=True,
)
assert automations_with_entity(hass, entity_id)[0] == "automation.test"
assert scripts_with_entity(hass, entity_id)[0] == "script.test"
assert len(issue_registry.issues) == 1
assert issue_registry.async_get_issue(DOMAIN, issue_id)
assert len(issue_registry.issues) == 2
assert issue_registry.async_get_issue(DOMAIN, automation_script_issue_id)
assert issue_registry.async_get_issue(DOMAIN, action_handler_issue_id)
await hass.config_entries.async_unload(config_entry.entry_id)
await hass.async_block_till_done()
# Assert the issue is no longer present
assert not issue_registry.async_get_issue(DOMAIN, issue_id)
assert not issue_registry.async_get_issue(DOMAIN, automation_script_issue_id)
assert not issue_registry.async_get_issue(DOMAIN, action_handler_issue_id)
assert len(issue_registry.issues) == 0
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
@pytest.mark.parametrize(
"service",
[SERVICE_TURN_ON, SERVICE_TURN_OFF],
)
async def test_program_switch_deprecation_issue_fix(
hass: HomeAssistant,
appliance: HomeAppliance,
service: str,
config_entry: MockConfigEntry,
integration_setup: Callable[[MagicMock], Awaitable[bool]],
setup_credentials: None,
client: MagicMock,
issue_registry: ir.IssueRegistry,
hass_client: ClientSessionGenerator,
) -> None:
"""Test we can fix the issues created when a program switch entity is in an automation or in a script or when is used."""
entity_id = "switch.washer_program_mix"
automation_script_issue_id = f"deprecated_program_switch_{entity_id}"
action_handler_issue_id = f"deprecated_program_switch_{entity_id}"
assert await async_setup_component(
hass,
automation.DOMAIN,
{
automation.DOMAIN: {
"alias": "test",
"trigger": {"platform": "state", "entity_id": entity_id},
"action": {
"action": "automation.turn_on",
"target": {
"entity_id": "automation.test",
},
},
}
},
)
assert await async_setup_component(
hass,
script.DOMAIN,
{
script.DOMAIN: {
"test": {
"sequence": [
{
"action": "switch.turn_on",
"entity_id": entity_id,
},
],
}
}
},
)
assert config_entry.state == ConfigEntryState.NOT_LOADED
assert await integration_setup(client)
assert config_entry.state == ConfigEntryState.LOADED
await hass.services.async_call(
SWITCH_DOMAIN,
service,
{
ATTR_ENTITY_ID: entity_id,
},
blocking=True,
)
assert automations_with_entity(hass, entity_id)[0] == "automation.test"
assert scripts_with_entity(hass, entity_id)[0] == "script.test"
assert len(issue_registry.issues) == 2
assert issue_registry.async_get_issue(DOMAIN, automation_script_issue_id)
assert issue_registry.async_get_issue(DOMAIN, action_handler_issue_id)
for issue in issue_registry.issues.copy().values():
_client = await hass_client()
resp = await _client.post(
"/api/repairs/issues/fix",
json={"handler": DOMAIN, "issue_id": issue.issue_id},
)
assert resp.status == HTTPStatus.OK
flow_id = (await resp.json())["flow_id"]
resp = await _client.post(f"/api/repairs/issues/fix/{flow_id}")
# Assert the issue is no longer present
assert not issue_registry.async_get_issue(DOMAIN, automation_script_issue_id)
assert not issue_registry.async_get_issue(DOMAIN, action_handler_issue_id)
assert len(issue_registry.issues) == 0

Some files were not shown because too many files have changed in this diff Show More