Compare commits

...

52 Commits

Author SHA1 Message Date
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 3766040960.
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
83 changed files with 1006 additions and 254 deletions
+17 -11
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
+5
View File
@@ -0,0 +1,5 @@
{
"domain": "bosch",
"name": "Bosch",
"integrations": ["bosch_alarm", "bosch_shc", "home_connect"]
}
@@ -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": {
@@ -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
@@ -20,6 +20,9 @@ CONNECTION_TEST_DATA: HassKey[dict[str, asyncio.Event]] = HassKey(
f"{DOMAIN}_connection_tests"
)
PREANNOUNCE_FILENAME = "preannounce.mp3"
PREANNOUNCE_URL = f"/api/assist_satellite/static/{PREANNOUNCE_FILENAME}"
class AssistSatelliteEntityFeature(IntFlag):
"""Supported features of Assist satellite entity."""
@@ -28,7 +28,7 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import chat_session, entity
from homeassistant.helpers.entity import EntityDescription
from .const import AssistSatelliteEntityFeature
from .const import PREANNOUNCE_URL, AssistSatelliteEntityFeature
from .errors import AssistSatelliteError, SatelliteBusyError
_LOGGER = logging.getLogger(__name__)
@@ -180,7 +180,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.
"""
@@ -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:
@@ -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']}",
)
+12 -2
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
@@ -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]
)
@@ -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"]
}
+10
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,
@@ -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"]
}
@@ -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}},
+2 -2
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"
@@ -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={
@@ -134,15 +134,47 @@
},
"deprecated_binary_common_door_sensor": {
"title": "Deprecated binary door sensor detected in some automations or scripts",
"description": "The binary door sensor `{entity}`, which is deprecated, is used in the following automations or scripts:\n{items}\n\nA sensor entity with additional possible states is available and should be used going forward; Please use it on the above automations or scripts to fix this issue."
"fix_flow": {
"step": {
"confirm": {
"title": "[%key:component::home_connect::issues::deprecated_binary_common_door_sensor::title%]",
"description": "The binary door sensor `{entity}`, which is deprecated, is used in the following automations or scripts:\n{items}\n\nA sensor entity with additional possible states is available and should be used going forward; Please use it on the above automations or scripts to fix this issue."
}
}
}
},
"deprecated_command_actions": {
"title": "The command related actions are deprecated in favor of the new buttons",
"description": "The `pause_program` and `resume_program` actions have been deprecated in favor of new button entities, if the command is available for your appliance. Please update your automations, scripts and panels that use this action to use the button entities instead, and press on submit to fix the issue."
"fix_flow": {
"step": {
"confirm": {
"title": "[%key:component::home_connect::issues::deprecated_command_actions::title%]",
"description": "The `pause_program` and `resume_program` actions have been deprecated in favor of new button entities, if the command is available for your appliance. Please update your automations, scripts and panels that use this action to use the button entities instead, and press on submit to fix the issue."
}
}
}
},
"deprecated_program_switch_in_automations_scripts": {
"title": "Deprecated program switch detected in some automations or scripts",
"fix_flow": {
"step": {
"confirm": {
"title": "[%key:component::home_connect::issues::deprecated_program_switch_in_automations_scripts::title%]",
"description": "Program switches are deprecated and {entity_id} is used in the following automations or scripts:\n{items}\n\nYou can use the active program select entity to run the program without any additional options and get the current running program on the above automations or scripts to fix this issue."
}
}
}
},
"deprecated_program_switch": {
"title": "Deprecated program switch detected in some automations or scripts",
"description": "Program switches are deprecated and {entity_id} is used in the following automations or scripts:\n{items}\n\nYou can use the active program select entity to run the program without any additional options and get the current running program on the above automations or scripts to fix this issue."
"title": "Deprecated program switch entities",
"fix_flow": {
"step": {
"confirm": {
"title": "[%key:component::home_connect::issues::deprecated_program_switch::title%]",
"description": "The switch entity `{entity_id}` and all the other program switches are deprecated.\n\nPlease use the active program select entity instead."
}
}
}
},
"deprecated_set_program_and_option_actions": {
"title": "The executed action is deprecated",
@@ -266,7 +266,10 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity):
super().__init__(
coordinator,
appliance,
SwitchEntityDescription(key=EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM),
SwitchEntityDescription(
key=EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM,
entity_registry_enabled_default=False,
),
)
self._attr_name = f"{appliance.info.name} {desc}"
self._attr_unique_id = f"{appliance.info.ha_id}-{desc}"
@@ -304,11 +307,12 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity):
async_create_issue(
self.hass,
DOMAIN,
f"deprecated_program_switch_{self.entity_id}",
f"deprecated_program_switch_in_automations_scripts_{self.entity_id}",
breaks_in_ha_version="2025.6.0",
is_fixable=False,
is_fixable=True,
is_persistent=True,
severity=IssueSeverity.WARNING,
translation_key="deprecated_program_switch",
translation_key="deprecated_program_switch_in_automations_scripts",
translation_placeholders={
"entity_id": self.entity_id,
"items": "\n".join(items_list),
@@ -317,12 +321,34 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity):
async def async_will_remove_from_hass(self) -> None:
"""Call when entity will be removed from hass."""
async_delete_issue(
self.hass,
DOMAIN,
f"deprecated_program_switch_in_automations_scripts_{self.entity_id}",
)
async_delete_issue(
self.hass, DOMAIN, f"deprecated_program_switch_{self.entity_id}"
)
def create_action_handler_issue(self) -> None:
"""Create deprecation issue."""
async_create_issue(
self.hass,
DOMAIN,
f"deprecated_program_switch_{self.entity_id}",
breaks_in_ha_version="2025.6.0",
is_fixable=True,
is_persistent=True,
severity=IssueSeverity.WARNING,
translation_key="deprecated_program_switch",
translation_placeholders={
"entity_id": self.entity_id,
},
)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Start the program."""
self.create_action_handler_issue()
try:
await self.coordinator.client.start_program(
self.appliance.info.ha_id, program_key=self.program.key
@@ -339,6 +365,7 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity):
async def async_turn_off(self, **kwargs: Any) -> None:
"""Stop the program."""
self.create_action_handler_issue()
try:
await self.coordinator.client.stop_program(self.appliance.info.ha_id)
except HomeConnectError as err:
+24
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"
@@ -227,12 +227,16 @@ def _get_work_area_names(data: MowerAttributes) -> list[str]:
@callback
def _get_current_work_area_name(data: MowerAttributes) -> str:
"""Return the name of the current work area."""
if data.mower.work_area_id is None:
return STATE_NO_WORK_AREA_ACTIVE
if TYPE_CHECKING:
# Sensor does not get created if values are None
assert data.work_areas is not None
return data.work_areas[data.mower.work_area_id].name
if (
data.mower.work_area_id is not None
and data.mower.work_area_id in data.work_areas
):
return data.work_areas[data.mower.work_area_id].name
return STATE_NO_WORK_AREA_ACTIVE
@callback
@@ -6,6 +6,7 @@ count_omer:
selector:
date:
nusach:
required: true
example: "sfarad"
default: "sfarad"
selector:
+17 -17
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%]"
+9 -1
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": {
+4 -1
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"
@@ -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."]
}
+1 -1
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:
@@ -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
+4 -10
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()
+3 -3
View File
@@ -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%]",
+1 -1
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
@@ -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"
}
}
}
@@ -31,6 +31,7 @@ class PyLoadData:
download: bool
reconnect: bool
captcha: bool | None = None
proxy: bool | None = None
free_space: int
@@ -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:
@@ -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={
+2 -2
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"
@@ -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 = {
@@ -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."""
@@ -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: [
@@ -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."
+1
View File
@@ -31,6 +31,7 @@ async def async_setup_entry(
"power",
"status_requested",
"sticky_white_noise_updated",
"config_change",
],
),
)
+1 -1
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"]
}
+2 -1
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"
}
}
}
@@ -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"
+11 -10
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
@@ -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."
}
+4 -3
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)
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aiowebdav2"],
"quality_scale": "bronze",
"requirements": ["aiowebdav2==0.4.2"]
"requirements": ["aiowebdav2==0.4.4"]
}
@@ -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)
+1 -1
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 = "0b7"
__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)
+22 -18
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",
+2 -2
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:
+1 -1
View File
@@ -38,7 +38,7 @@ 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-frontend==20250328.0
home-assistant-intents==2025.3.24
httpx==0.28.1
ifaddr==0.2.0
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2025.4.0.dev0"
version = "2025.4.0b7"
license = "Apache-2.0"
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
description = "Open-source home automation platform running on Python 3."
+5 -5
View File
@@ -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,7 +1157,7 @@ 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
@@ -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
+5 -5
View File
@@ -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,7 +984,7 @@ 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
@@ -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
@@ -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:
@@ -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(
+1 -1
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",
{
+116 -2
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,
@@ -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,
)
+22 -3
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"
@@ -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,
+15 -15
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")
@@ -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
+118 -6
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
+4 -4
View File
@@ -320,7 +320,7 @@ async def test_time_entity_error(
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
@pytest.mark.parametrize("appliance", ["Oven"], indirect=True)
async def test_create_issue(
async def test_create_alarm_clock_deprecation_issue(
hass: HomeAssistant,
appliance: HomeAppliance,
config_entry: MockConfigEntry,
@@ -329,7 +329,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 alarm clock time entity or the entity is used by the user."""
entity_id = f"{TIME_DOMAIN}.oven_alarm_clock"
automation_script_issue_id = (
f"deprecated_time_alarm_clock_in_automations_scripts_{entity_id}"
@@ -401,7 +401,7 @@ async def test_create_issue(
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
@pytest.mark.parametrize("appliance", ["Oven"], indirect=True)
async def test_issue_fix(
async def test_alarm_clock_deprecation_issue_fix(
hass: HomeAssistant,
appliance: HomeAppliance,
config_entry: MockConfigEntry,
@@ -411,7 +411,7 @@ async def test_issue_fix(
issue_registry: ir.IssueRegistry,
hass_client: ClientSessionGenerator,
) -> None:
"""Test we create an issue when an automation or script is using a deprecated entity."""
"""Test we can fix the issues created when a alarm clock time entity is in an automation or in a script or when is used."""
entity_id = f"{TIME_DOMAIN}.oven_alarm_clock"
automation_script_issue_id = (
f"deprecated_time_alarm_clock_in_automations_scripts_{entity_id}"
@@ -110,6 +110,18 @@ async def test_work_area_sensor(
state = hass.states.get("sensor.test_mower_1_work_area")
assert state.state == "my_lawn"
# Test EPOS mower, which returns work_area_id = 0, when no
# work area is active and has no default work_area_id=0
values[TEST_MOWER_ID].mower.work_area_id = 0
del values[TEST_MOWER_ID].work_areas[0]
del values[TEST_MOWER_ID].work_area_dict[0]
mock_automower_client.get_status.return_value = values
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
state = hass.states.get("sensor.test_mower_1_work_area")
assert state.state == "no_work_area_active"
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
@pytest.mark.parametrize(
@@ -72,7 +72,6 @@
"1/59/0": 2,
"1/59/65533": 1,
"1/59/1": 0,
"1/59/2": 2,
"1/59/65531": [0, 1, 65528, 65529, 65531, 65532, 65533],
"1/59/65532": 30,
"1/59/65528": [],
@@ -102,7 +101,7 @@
"2/59/0": 2,
"2/59/65533": 1,
"2/59/1": 0,
"2/59/2": 2,
"2/59/2": 4,
"2/59/65531": [0, 1, 65528, 65529, 65531, 65532, 65533],
"2/59/65532": 30,
"2/59/65528": [],
@@ -132,6 +132,8 @@
'event_types': list([
'multi_press_1',
'multi_press_2',
'multi_press_3',
'multi_press_4',
'long_press',
'long_release',
]),
@@ -172,6 +174,8 @@
'event_types': list([
'multi_press_1',
'multi_press_2',
'multi_press_3',
'multi_press_4',
'long_press',
'long_release',
]),
@@ -686,7 +686,7 @@
'state': '20.0',
})
# ---
# name: test_sensors[air_purifier][sensor.air_purifier_vocs-entry]
# name: test_sensors[air_purifier][sensor.air_purifier_volatile_organic_compounds_parts-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -701,7 +701,7 @@
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.air_purifier_vocs',
'entity_id': 'sensor.air_purifier_volatile_organic_compounds_parts',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
@@ -713,7 +713,7 @@
}),
'original_device_class': <SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS: 'volatile_organic_compounds_parts'>,
'original_icon': None,
'original_name': 'VOCs',
'original_name': 'Volatile organic compounds parts',
'platform': 'matter',
'previous_unique_id': None,
'supported_features': 0,
@@ -722,16 +722,16 @@
'unit_of_measurement': 'ppm',
})
# ---
# name: test_sensors[air_purifier][sensor.air_purifier_vocs-state]
# name: test_sensors[air_purifier][sensor.air_purifier_volatile_organic_compounds_parts-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'volatile_organic_compounds_parts',
'friendly_name': 'Air Purifier VOCs',
'friendly_name': 'Air Purifier Volatile organic compounds parts',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'ppm',
}),
'context': <ANY>,
'entity_id': 'sensor.air_purifier_vocs',
'entity_id': 'sensor.air_purifier_volatile_organic_compounds_parts',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
@@ -1167,7 +1167,7 @@
'state': '20.08',
})
# ---
# name: test_sensors[air_quality_sensor][sensor.lightfi_aq1_air_quality_sensor_vocs-entry]
# name: test_sensors[air_quality_sensor][sensor.lightfi_aq1_air_quality_sensor_volatile_organic_compounds_parts-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
@@ -1182,7 +1182,7 @@
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.lightfi_aq1_air_quality_sensor_vocs',
'entity_id': 'sensor.lightfi_aq1_air_quality_sensor_volatile_organic_compounds_parts',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
@@ -1194,7 +1194,7 @@
}),
'original_device_class': <SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS: 'volatile_organic_compounds_parts'>,
'original_icon': None,
'original_name': 'VOCs',
'original_name': 'Volatile organic compounds parts',
'platform': 'matter',
'previous_unique_id': None,
'supported_features': 0,
@@ -1203,16 +1203,16 @@
'unit_of_measurement': 'ppm',
})
# ---
# name: test_sensors[air_quality_sensor][sensor.lightfi_aq1_air_quality_sensor_vocs-state]
# name: test_sensors[air_quality_sensor][sensor.lightfi_aq1_air_quality_sensor_volatile_organic_compounds_parts-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'volatile_organic_compounds_parts',
'friendly_name': 'lightfi-aq1-air-quality-sensor VOCs',
'friendly_name': 'lightfi-aq1-air-quality-sensor Volatile organic compounds parts',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': 'ppm',
}),
'context': <ANY>,
'entity_id': 'sensor.lightfi_aq1_air_quality_sensor_vocs',
'entity_id': 'sensor.lightfi_aq1_air_quality_sensor_volatile_organic_compounds_parts',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
+15 -6
View File
@@ -36,7 +36,7 @@ async def test_generic_switch_node(
assert state
assert state.state == "unknown"
assert state.name == "Mock Generic Switch Button"
# check event_types from featuremap 30
# check event_types from featuremap 14 (0b1110)
assert state.attributes[ATTR_EVENT_TYPES] == [
"initial_press",
"short_release",
@@ -76,7 +76,7 @@ async def test_generic_switch_multi_node(
assert state_button_1.state == "unknown"
# name should be 'DeviceName Button (1)' due to the label set to just '1'
assert state_button_1.name == "Mock Generic Switch Button (1)"
# check event_types from featuremap 14
# check event_types from featuremap 30 (0b11110) and MultiPressMax unset (default 2)
assert state_button_1.attributes[ATTR_EVENT_TYPES] == [
"multi_press_1",
"multi_press_2",
@@ -84,11 +84,20 @@ async def test_generic_switch_multi_node(
"long_release",
]
# check button 2
state_button_1 = hass.states.get("event.mock_generic_switch_fancy_button")
assert state_button_1
assert state_button_1.state == "unknown"
state_button_2 = hass.states.get("event.mock_generic_switch_fancy_button")
assert state_button_2
assert state_button_2.state == "unknown"
# name should be 'DeviceName Fancy Button' due to the label set to 'Fancy Button'
assert state_button_1.name == "Mock Generic Switch Fancy Button"
assert state_button_2.name == "Mock Generic Switch Fancy Button"
# check event_types from featuremap 30 (0b11110) and MultiPressMax 4
assert state_button_2.attributes[ATTR_EVENT_TYPES] == [
"multi_press_1",
"multi_press_2",
"multi_press_3",
"multi_press_4",
"long_press",
"long_release",
]
# trigger firing a multi press event
await trigger_subscription_callback(
@@ -2908,6 +2908,10 @@ async def test_subentry_configflow(
iter(config_subentries_data["components"].values())
)
subentry_device_data = next(iter(config_entry.subentries.values())).data["device"]
for option, value in mock_device_user_input.items():
assert subentry_device_data[option] == value
await hass.async_block_till_done()
+13 -12
View File
@@ -30,8 +30,8 @@ async def test_sensors(
) -> None:
"""Test the PVOutput sensors."""
state = hass.states.get("sensor.frenck_s_solar_farm_energy_consumed")
entry = entity_registry.async_get("sensor.frenck_s_solar_farm_energy_consumed")
state = hass.states.get("sensor.frenck_s_solar_farm_energy_consumption")
entry = entity_registry.async_get("sensor.frenck_s_solar_farm_energy_consumption")
assert entry
assert state
assert entry.unique_id == "12345_energy_consumption"
@@ -40,14 +40,14 @@ async def test_sensors(
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY
assert (
state.attributes.get(ATTR_FRIENDLY_NAME)
== "Frenck's Solar Farm Energy consumed"
== "Frenck's Solar Farm Energy consumption"
)
assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL_INCREASING
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.WATT_HOUR
assert ATTR_ICON not in state.attributes
state = hass.states.get("sensor.frenck_s_solar_farm_energy_generated")
entry = entity_registry.async_get("sensor.frenck_s_solar_farm_energy_generated")
state = hass.states.get("sensor.frenck_s_solar_farm_energy_generation")
entry = entity_registry.async_get("sensor.frenck_s_solar_farm_energy_generation")
assert entry
assert state
assert entry.unique_id == "12345_energy_generation"
@@ -56,7 +56,7 @@ async def test_sensors(
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY
assert (
state.attributes.get(ATTR_FRIENDLY_NAME)
== "Frenck's Solar Farm Energy generated"
== "Frenck's Solar Farm Energy generation"
)
assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL_INCREASING
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.WATT_HOUR
@@ -78,8 +78,8 @@ async def test_sensors(
assert ATTR_DEVICE_CLASS not in state.attributes
assert ATTR_ICON not in state.attributes
state = hass.states.get("sensor.frenck_s_solar_farm_power_consumed")
entry = entity_registry.async_get("sensor.frenck_s_solar_farm_power_consumed")
state = hass.states.get("sensor.frenck_s_solar_farm_power_consumption")
entry = entity_registry.async_get("sensor.frenck_s_solar_farm_power_consumption")
assert entry
assert state
assert entry.unique_id == "12345_power_consumption"
@@ -87,14 +87,15 @@ async def test_sensors(
assert state.state == "2500.0"
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER
assert (
state.attributes.get(ATTR_FRIENDLY_NAME) == "Frenck's Solar Farm Power consumed"
state.attributes.get(ATTR_FRIENDLY_NAME)
== "Frenck's Solar Farm Power consumption"
)
assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT
assert ATTR_ICON not in state.attributes
state = hass.states.get("sensor.frenck_s_solar_farm_power_generated")
entry = entity_registry.async_get("sensor.frenck_s_solar_farm_power_generated")
state = hass.states.get("sensor.frenck_s_solar_farm_power_generation")
entry = entity_registry.async_get("sensor.frenck_s_solar_farm_power_generation")
assert entry
assert state
assert entry.unique_id == "12345_power_generation"
@@ -103,7 +104,7 @@ async def test_sensors(
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.POWER
assert (
state.attributes.get(ATTR_FRIENDLY_NAME)
== "Frenck's Solar Farm Power generated"
== "Frenck's Solar Farm Power generation"
)
assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfPower.WATT
@@ -13,6 +13,7 @@
'download': True,
'free_space': 99999999999,
'pause': False,
'proxy': None,
'queue': 6,
'reconnect': False,
'speed': 5405963.0,
@@ -45,6 +45,35 @@ async def test_form_import_ics(hass: HomeAssistant, ics_content: str) -> None:
}
@respx.mock
async def test_form_import_webcal(hass: HomeAssistant, ics_content: str) -> None:
"""Test we get the import form."""
respx.get(CALENDER_URL).mock(
return_value=Response(
status_code=200,
text=ics_content,
)
)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={
CONF_CALENDAR_NAME: CALENDAR_NAME,
CONF_URL: "webcal://some.calendar.com/calendar.ics",
},
)
assert result2["type"] is FlowResultType.CREATE_ENTRY
assert result2["title"] == CALENDAR_NAME
assert result2["data"] == {
CONF_CALENDAR_NAME: CALENDAR_NAME,
CONF_URL: CALENDER_URL,
}
@pytest.mark.parametrize(
("side_effect"),
[
@@ -7,7 +7,7 @@
'capabilities': dict({
'max': 5,
'min': 0,
'mode': <NumberMode.AUTO: 'auto'>,
'mode': <NumberMode.BOX: 'box'>,
'step': 1.0,
}),
'config_entry_id': <ANY>,
@@ -44,7 +44,7 @@
'friendly_name': 'Washer Rinse cycles',
'max': 5,
'min': 0,
'mode': <NumberMode.AUTO: 'auto'>,
'mode': <NumberMode.BOX: 'box'>,
'step': 1.0,
'unit_of_measurement': 'cycles',
}),
@@ -64,7 +64,7 @@
'capabilities': dict({
'max': 5,
'min': 0,
'mode': <NumberMode.AUTO: 'auto'>,
'mode': <NumberMode.BOX: 'box'>,
'step': 1.0,
}),
'config_entry_id': <ANY>,
@@ -101,7 +101,7 @@
'friendly_name': 'Washing Machine Rinse cycles',
'max': 5,
'min': 0,
'mode': <NumberMode.AUTO: 'auto'>,
'mode': <NumberMode.BOX: 'box'>,
'step': 1.0,
'unit_of_measurement': 'cycles',
}),
+66 -2
View File
@@ -529,12 +529,28 @@ async def test_entity_unique_id_migration(
"microwave_machine_state",
"2bad3237-4886-e699-1b90-4a51a3d55c8a_main_ovenOperatingState_machineState_machineState",
),
(
"da_ks_microwave_0101x",
SENSOR_DOMAIN,
"2bad3237-4886-e699-1b90-4a51a3d55c8a.ovenJobState",
"2bad3237-4886-e699-1b90-4a51a3d55c8a.completionTime",
"microwave_completion_time",
"2bad3237-4886-e699-1b90-4a51a3d55c8a_main_ovenOperatingState_completionTime_completionTime",
),
(
"da_ks_microwave_0101x",
SENSOR_DOMAIN,
"2bad3237-4886-e699-1b90-4a51a3d55c8a_main_ovenOperatingState_ovenJobState_ovenJobState",
"2bad3237-4886-e699-1b90-4a51a3d55c8a.completionTime",
"microwave_completion_time",
"2bad3237-4886-e699-1b90-4a51a3d55c8a_main_ovenOperatingState_completionTime_completionTime",
),
(
"da_wm_dw_000001",
SENSOR_DOMAIN,
"f36dc7ce-cac0-0667-dc14-a3704eb5e676.dishwasherJobState",
"f36dc7ce-cac0-0667-dc14-a3704eb5e676.machineState",
"microwave_machine_state",
"dishwasher_machine_state",
"f36dc7ce-cac0-0667-dc14-a3704eb5e676_main_dishwasherOperatingState_machineState_machineState",
),
(
@@ -542,9 +558,25 @@ async def test_entity_unique_id_migration(
SENSOR_DOMAIN,
"f36dc7ce-cac0-0667-dc14-a3704eb5e676_main_dishwasherOperatingState_dishwasherJobState_dishwasherJobState",
"f36dc7ce-cac0-0667-dc14-a3704eb5e676.machineState",
"microwave_machine_state",
"dishwasher_machine_state",
"f36dc7ce-cac0-0667-dc14-a3704eb5e676_main_dishwasherOperatingState_machineState_machineState",
),
(
"da_wm_dw_000001",
SENSOR_DOMAIN,
"f36dc7ce-cac0-0667-dc14-a3704eb5e676.dishwasherJobState",
"f36dc7ce-cac0-0667-dc14-a3704eb5e676.completionTime",
"dishwasher_completion_time",
"f36dc7ce-cac0-0667-dc14-a3704eb5e676_main_dishwasherOperatingState_completionTime_completionTime",
),
(
"da_wm_dw_000001",
SENSOR_DOMAIN,
"f36dc7ce-cac0-0667-dc14-a3704eb5e676_main_dishwasherOperatingState_dishwasherJobState_dishwasherJobState",
"f36dc7ce-cac0-0667-dc14-a3704eb5e676.completionTime",
"dishwasher_completion_time",
"f36dc7ce-cac0-0667-dc14-a3704eb5e676_main_dishwasherOperatingState_completionTime_completionTime",
),
(
"da_wm_wd_000001",
SENSOR_DOMAIN,
@@ -561,6 +593,22 @@ async def test_entity_unique_id_migration(
"dryer_machine_state",
"02f7256e-8353-5bdd-547f-bd5b1647e01b_main_dryerOperatingState_machineState_machineState",
),
(
"da_wm_wd_000001",
SENSOR_DOMAIN,
"02f7256e-8353-5bdd-547f-bd5b1647e01b.dryerJobState",
"02f7256e-8353-5bdd-547f-bd5b1647e01b.completionTime",
"dryer_completion_time",
"02f7256e-8353-5bdd-547f-bd5b1647e01b_main_dryerOperatingState_completionTime_completionTime",
),
(
"da_wm_wd_000001",
SENSOR_DOMAIN,
"02f7256e-8353-5bdd-547f-bd5b1647e01b_main_dryerOperatingState_dryerJobState_dryerJobState",
"02f7256e-8353-5bdd-547f-bd5b1647e01b.completionTime",
"dryer_completion_time",
"02f7256e-8353-5bdd-547f-bd5b1647e01b_main_dryerOperatingState_completionTime_completionTime",
),
(
"da_wm_wm_000001",
SENSOR_DOMAIN,
@@ -577,6 +625,22 @@ async def test_entity_unique_id_migration(
"washer_machine_state",
"f984b91d-f250-9d42-3436-33f09a422a47_main_washerOperatingState_machineState_machineState",
),
(
"da_wm_wm_000001",
SENSOR_DOMAIN,
"f984b91d-f250-9d42-3436-33f09a422a47.washerJobState",
"f984b91d-f250-9d42-3436-33f09a422a47.completionTime",
"washer_completion_time",
"f984b91d-f250-9d42-3436-33f09a422a47_main_washerOperatingState_completionTime_completionTime",
),
(
"da_wm_wm_000001",
SENSOR_DOMAIN,
"f984b91d-f250-9d42-3436-33f09a422a47_main_washerOperatingState_washerJobState_washerJobState",
"f984b91d-f250-9d42-3436-33f09a422a47.completionTime",
"washer_completion_time",
"f984b91d-f250-9d42-3436-33f09a422a47_main_washerOperatingState_completionTime_completionTime",
),
],
)
async def test_entity_unique_id_migration_machine_state(
@@ -60,8 +60,6 @@ async def test_state_update(
@pytest.mark.parametrize(
("device_fixture", "entity_id", "translation_key"),
[
("da_wm_wm_000001", "sensor.washer_machine_state", "machine_state"),
("da_wm_wd_000001", "sensor.dryer_machine_state", "machine_state"),
("hw_q80r_soundbar", "sensor.soundbar_volume", "media_player"),
("hw_q80r_soundbar", "sensor.soundbar_media_playback_status", "media_player"),
("hw_q80r_soundbar", "sensor.soundbar_media_input_source", "media_player"),
+14 -7
View File
@@ -234,13 +234,19 @@ async def test_homekit(hass: HomeAssistant, mock_tado_api: MagicMock) -> None:
type="mock_type",
),
)
assert result["type"] is FlowResultType.SHOW_PROGRESS_DONE
flow = next(
flow
for flow in hass.config_entries.flow.async_progress()
if flow["flow_id"] == result["flow_id"]
)
assert flow["context"]["unique_id"] == "AA:BB:CC:DD:EE:FF"
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "homekit_confirm"
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["result"].unique_id == "1"
async def test_homekit_already_setup(
hass: HomeAssistant, mock_tado_api: MagicMock
) -> None:
"""Test that we abort from homekit if tado is already setup."""
entry = MockConfigEntry(
domain=DOMAIN, data={CONF_USERNAME: "mock", CONF_PASSWORD: "mock"}
@@ -261,3 +267,4 @@ async def test_homekit(hass: HomeAssistant, mock_tado_api: MagicMock) -> None:
),
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
+43
View File
@@ -8,6 +8,7 @@ from syrupy.assertion import SnapshotAssertion
from homeassistant.components import switch, template
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.components.template.switch import rewrite_legacy_to_modern_conf
from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import (
ATTR_ENTITY_ID,
SERVICE_TURN_OFF,
@@ -17,6 +18,7 @@ from homeassistant.const import (
STATE_UNAVAILABLE,
)
from homeassistant.core import CoreState, HomeAssistant, ServiceCall, State
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.template import Template
from homeassistant.setup import async_setup_component
@@ -29,6 +31,7 @@ from tests.common import (
mock_component,
mock_restore_cache,
)
from tests.typing import WebSocketGenerator
TEST_OBJECT_ID = "test_template_switch"
TEST_ENTITY_ID = f"switch.{TEST_OBJECT_ID}"
@@ -279,6 +282,46 @@ async def test_setup_config_entry(
assert state == snapshot
@pytest.mark.parametrize("state_key", ["value_template", "state"])
async def test_flow_preview(
hass: HomeAssistant,
state_key: str,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test the config flow preview."""
client = await hass_ws_client(hass)
result = await hass.config_entries.flow.async_init(
template.DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.MENU
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{"next_step_id": SWITCH_DOMAIN},
)
await hass.async_block_till_done()
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == SWITCH_DOMAIN
assert result["errors"] is None
assert result["preview"] == "template"
await client.send_json_auto_id(
{
"type": "template/start_preview",
"flow_id": result["flow_id"],
"flow_type": "config_flow",
"user_input": {"name": "My template", state_key: "{{ 'on' }}"},
}
)
msg = await client.receive_json()
assert msg["success"]
assert msg["result"] is None
msg = await client.receive_json()
assert msg["event"]["state"] == "on"
@pytest.mark.parametrize(
("count", "state_template"), [(1, "{{ states.switch.test_state.state }}")]
)
+12 -3
View File
@@ -5853,14 +5853,16 @@ async def test_stop_action_subscript(
)
@pytest.mark.parametrize(("var", "response"), [(1, "If: Then"), (2, "Testing 123")])
@pytest.mark.parametrize(
("var", "response"),
[(1, "If: Then"), (2, "Testing 123")],
("script_mode", "max_runs"), [("single", 1), ("parallel", 2), ("queued", 2)]
)
async def test_stop_action_response_variables(
hass: HomeAssistant,
var: int,
response: str,
script_mode,
max_runs,
) -> None:
"""Test setting stop response_variable in a subscript."""
sequence = cv.SCRIPT_SCHEMA(
@@ -5879,7 +5881,14 @@ async def test_stop_action_response_variables(
{"stop": "In the name of love", "response_variable": "output"},
]
)
script_obj = script.Script(hass, sequence, "Test Name", "test_domain")
script_obj = script.Script(
hass,
sequence,
"Test Name",
"test_domain",
script_mode=script_mode,
max_runs=max_runs,
)
run_vars = MappingProxyType({"var": var})
result = await script_obj.async_run(run_vars, context=Context())
+8 -10
View File
@@ -252,8 +252,8 @@ async def test_setup_after_deps_all_present(hass: HomeAssistant) -> None:
@pytest.mark.parametrize("load_registries", [False])
async def test_setup_after_deps_in_stage_1(hass: HomeAssistant) -> None:
"""Test after_dependencies are promoted in stage 1."""
async def test_setup_after_deps_in_stage_1_ignored(hass: HomeAssistant) -> None:
"""Test after_dependencies are ignored in stage 1."""
# This test relies on this
assert "cloud" in bootstrap.STAGE_1_INTEGRATIONS
order = []
@@ -295,7 +295,7 @@ async def test_setup_after_deps_in_stage_1(hass: HomeAssistant) -> None:
assert "normal_integration" in hass.config.components
assert "cloud" in hass.config.components
assert order == ["an_after_dep", "normal_integration", "cloud"]
assert order == ["cloud", "an_after_dep", "normal_integration"]
@pytest.mark.parametrize("load_registries", [False])
@@ -304,7 +304,7 @@ async def test_setup_after_deps_manifests_are_loaded_even_if_not_setup(
) -> None:
"""Ensure we preload manifests for after deps even if they are not setup.
It's important that we preload the after dep manifests even if they are not setup
Its important that we preload the after dep manifests even if they are not setup
since we will always have to check their requirements since any integration
that lists an after dep may import it and we have to ensure requirements are
up to date before the after dep can be imported.
@@ -371,7 +371,7 @@ async def test_setup_after_deps_manifests_are_loaded_even_if_not_setup(
assert "an_after_dep" not in hass.config.components
assert "an_after_dep_of_after_dep" not in hass.config.components
assert "an_after_dep_of_after_dep_of_after_dep" not in hass.config.components
assert order == ["normal_integration", "cloud"]
assert order == ["cloud", "normal_integration"]
assert loader.async_get_loaded_integration(hass, "an_after_dep") is not None
assert (
loader.async_get_loaded_integration(hass, "an_after_dep_of_after_dep")
@@ -456,9 +456,9 @@ async def test_setup_frontend_before_recorder(hass: HomeAssistant) -> None:
assert order == [
"http",
"an_after_dep",
"frontend",
"recorder",
"an_after_dep",
"normal_integration",
]
@@ -1577,10 +1577,8 @@ async def test_no_base_platforms_loaded_before_recorder(hass: HomeAssistant) ->
assert not isinstance(integrations_or_excs, Exception)
integrations[domain] = integration
integrations_all_dependencies = (
await loader.resolve_integrations_after_dependencies(
hass, integrations.values(), ignore_exceptions=True
)
integrations_all_dependencies = await loader.resolve_integrations_dependencies(
hass, integrations.values()
)
all_integrations = integrations.copy()
all_integrations.update(