forked from home-assistant/core
Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d1512d46be | |||
| 0be7db6270 | |||
| 2af0282725 | |||
| ff458c8417 | |||
| cc93152ff0 | |||
| 9965f01609 | |||
| e9c76ce694 | |||
| 58ab7d350d | |||
| e4d6e20ebd | |||
| 45e273897a | |||
| d9ec7142d7 | |||
| e162499267 | |||
| 67f21429e3 | |||
| a0563f06c9 | |||
| e7c4fdc8bb | |||
| c490e350bc | |||
| e11409ef99 | |||
| 5c8e415a76 | |||
| e795fb9497 | |||
| d0afabb85c | |||
| 4f3e8e9b94 | |||
| 46c1cbbc9c | |||
| 8d9a4ea278 | |||
| 22c83e2393 | |||
| c83a75f6f9 | |||
| 841c727112 | |||
| d8c9655bfd | |||
| 942ed89cc4 | |||
| a1fe6b9cf3 | |||
| 2567181cc2 | |||
| 028e4f6029 | |||
| b82e1a9bef | |||
| 438f226c31 | |||
| 2f139e3cb1 | |||
| 5d75e96fbf | |||
| dcf2ec5c37 | |||
| 2431e1ba98 | |||
| 4ead108c15 | |||
| ec8363fa49 | |||
| e7ff0a3f8b | |||
| f4c0eb4189 | |||
| b1ee5a76e1 | |||
| 6b9e8c301b | |||
| 89c3266c7e | |||
| cff0a632e8 | |||
| e04d8557ae | |||
| ca6286f241 | |||
| 35bcc9d5af | |||
| 25b45ce867 | |||
| d568209bd5 | |||
| 8a43e8af9e | |||
| 785e5b2c16 |
+17
-11
@@ -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
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
Binary file not shown.
@@ -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']}",
|
||||
)
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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}},
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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%]"
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
{
|
||||
"entity_component": {
|
||||
"_": {
|
||||
"default": "mdi:lightbulb"
|
||||
"default": "mdi:lightbulb",
|
||||
"state_attributes": {
|
||||
"effect": {
|
||||
"default": "mdi:circle-medium",
|
||||
"state": {
|
||||
"off": "mdi:star-off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
|
||||
@@ -93,7 +93,10 @@
|
||||
"name": "Color temperature (Kelvin)"
|
||||
},
|
||||
"effect": {
|
||||
"name": "Effect"
|
||||
"name": "Effect",
|
||||
"state": {
|
||||
"off": "[%key:common::state::off%]"
|
||||
}
|
||||
},
|
||||
"effect_list": {
|
||||
"name": "Available effects"
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["linkplay"],
|
||||
"requirements": ["python-linkplay==0.2.1"],
|
||||
"requirements": ["python-linkplay==0.2.2"],
|
||||
"zeroconf": ["_linkplay._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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%]",
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -31,6 +31,7 @@ async def async_setup_entry(
|
||||
"power",
|
||||
"status_requested",
|
||||
"sticky_white_noise_updated",
|
||||
"config_change",
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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."
|
||||
|
||||
Generated
+5
-5
@@ -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
|
||||
|
||||
Generated
+5
-5
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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',
|
||||
}),
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 }}")]
|
||||
)
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user