mirror of
https://github.com/home-assistant/core.git
synced 2025-12-04 06:58:33 +00:00
Compare commits
12 Commits
services_f
...
knx-data-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d979a1d7f | ||
|
|
0a1214256a | ||
|
|
1a60c46d67 | ||
|
|
62fba5ca20 | ||
|
|
b54cde795c | ||
|
|
0f456373bf | ||
|
|
a5042027b8 | ||
|
|
b15b5ba95c | ||
|
|
cd6e72798e | ||
|
|
739157e59f | ||
|
|
267aa1af42 | ||
|
|
7328b61a69 |
@@ -8,6 +8,8 @@
|
||||
"integration_type": "system",
|
||||
"preview_features": {
|
||||
"new_triggers_conditions": {
|
||||
"feedback_url": "https://forms.gle/fWFZqf5MzuwWTsCH8",
|
||||
"learn_more_url": "https://www.home-assistant.io/blog/2025/12/03/release-202512/#purpose-specific-triggers-and-conditions",
|
||||
"report_issue_url": "https://github.com/home-assistant/core/issues/new?template=bug_report.yml&integration_link=https://www.home-assistant.io/integrations/automation&integration_name=Automation"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -407,8 +407,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
return [
|
||||
RTCIceServer(
|
||||
urls=[
|
||||
"stun:stun.home-assistant.io:80",
|
||||
"stun:stun.home-assistant.io:3478",
|
||||
"stun:stun.home-assistant.io:80",
|
||||
]
|
||||
),
|
||||
]
|
||||
|
||||
@@ -561,7 +561,7 @@ class BaseCloudLLMEntity(Entity):
|
||||
"schema": _format_structured_output(
|
||||
structure, chat_log.llm_api
|
||||
),
|
||||
"strict": True,
|
||||
"strict": False,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,6 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
||||
"requirements": ["hass-nabucasa==1.6.2"],
|
||||
"requirements": ["hass-nabucasa==1.7.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ DEFAULT_TTS_MODEL = "eleven_multilingual_v2"
|
||||
DEFAULT_STABILITY = 0.5
|
||||
DEFAULT_SIMILARITY = 0.75
|
||||
DEFAULT_STT_AUTO_LANGUAGE = False
|
||||
DEFAULT_STT_MODEL = "scribe_v1"
|
||||
DEFAULT_STT_MODEL = "scribe_v2"
|
||||
DEFAULT_STYLE = 0
|
||||
DEFAULT_USE_SPEAKER_BOOST = True
|
||||
|
||||
@@ -129,4 +129,5 @@ STT_LANGUAGES = [
|
||||
STT_MODELS = {
|
||||
"scribe_v1": "Scribe v1",
|
||||
"scribe_v1_experimental": "Scribe v1 Experimental",
|
||||
"scribe_v2": "Scribe v2 Realtime",
|
||||
}
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["iometer==0.2.0"],
|
||||
"requirements": ["iometer==0.3.0"],
|
||||
"zeroconf": ["_iometer._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
"telegram_count": {
|
||||
"default": "mdi:plus-network"
|
||||
},
|
||||
"telegrams_data_secure_undecodable": {
|
||||
"default": "mdi:lock-alert"
|
||||
},
|
||||
"telegrams_incoming": {
|
||||
"default": "mdi:upload-network"
|
||||
},
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"loggers": ["xknx", "xknxproject"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": [
|
||||
"xknx==3.11.0",
|
||||
"xknx==3.12.0",
|
||||
"xknxproject==3.8.2",
|
||||
"knx-frontend==2025.10.31.195356"
|
||||
],
|
||||
|
||||
@@ -108,6 +108,12 @@ SYSTEM_ENTITY_DESCRIPTIONS = (
|
||||
+ knx.xknx.connection_manager.cemi_count_incoming
|
||||
+ knx.xknx.connection_manager.cemi_count_incoming_error,
|
||||
),
|
||||
KNXSystemEntityDescription(
|
||||
key="telegrams_data_secure_undecodable",
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
value_fn=lambda knx: knx.xknx.connection_manager.undecoded_data_secure,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -639,6 +639,10 @@
|
||||
"name": "Telegrams",
|
||||
"unit_of_measurement": "telegrams"
|
||||
},
|
||||
"telegrams_data_secure_undecodable": {
|
||||
"name": "Undecodable Data Secure telegrams",
|
||||
"unit_of_measurement": "[%key:component::knx::entity::sensor::telegrams_incoming_error::unit_of_measurement%]"
|
||||
},
|
||||
"telegrams_incoming": {
|
||||
"name": "Incoming telegrams",
|
||||
"unit_of_measurement": "[%key:component::knx::entity::sensor::telegram_count::unit_of_measurement%]"
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aioonkyo"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["aioonkyo==0.3.0"],
|
||||
"requirements": ["aioonkyo==0.4.0"],
|
||||
"ssdp": [
|
||||
{
|
||||
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"config_flow": true,
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/oralb",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["oralb_ble"],
|
||||
"requirements": ["oralb-ble==0.17.6"]
|
||||
|
||||
@@ -24,14 +24,9 @@ from homeassistant.helpers.trigger import (
|
||||
async_get_all_descriptions as async_get_all_trigger_descriptions,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
FLATTENED_SERVICE_DESCRIPTIONS_CACHE: HassKey[
|
||||
tuple[dict[str, dict[str, Any]], dict[str, dict[str, Any] | None]]
|
||||
] = HassKey("websocket_automation_flat_service_description_cache")
|
||||
|
||||
|
||||
@dataclass(slots=True, kw_only=True)
|
||||
class _EntityFilter:
|
||||
@@ -222,29 +217,12 @@ async def async_get_services_for_target(
|
||||
) -> set[str]:
|
||||
"""Get services for a target."""
|
||||
descriptions = await async_get_all_service_descriptions(hass)
|
||||
|
||||
def get_flattened_service_descriptions() -> dict[str, dict[str, Any] | None]:
|
||||
"""Get flattened service descriptions, with caching."""
|
||||
if FLATTENED_SERVICE_DESCRIPTIONS_CACHE in hass.data:
|
||||
cached_descriptions, cached_flat_descriptions = hass.data[
|
||||
FLATTENED_SERVICE_DESCRIPTIONS_CACHE
|
||||
]
|
||||
# If the descriptions are the same, return the cached flattened version
|
||||
if cached_descriptions is descriptions:
|
||||
return cached_flat_descriptions
|
||||
|
||||
# Flatten dicts to be keyed by domain.name to match trigger/condition format
|
||||
flat_descriptions = {
|
||||
f"{domain}.{service_name}": desc
|
||||
for domain, services in descriptions.items()
|
||||
for service_name, desc in services.items()
|
||||
}
|
||||
hass.data[FLATTENED_SERVICE_DESCRIPTIONS_CACHE] = (
|
||||
descriptions,
|
||||
flat_descriptions,
|
||||
)
|
||||
return flat_descriptions
|
||||
|
||||
# Flatten dicts to be keyed by domain.name to match trigger/condition format
|
||||
descriptions_flatten = {
|
||||
f"{domain}.{service_name}": desc
|
||||
for domain, services in descriptions.items()
|
||||
for service_name, desc in services.items()
|
||||
}
|
||||
return _async_get_automation_components_for_target(
|
||||
hass, target_selector, expand_group, get_flattened_service_descriptions()
|
||||
hass, target_selector, expand_group, descriptions_flatten
|
||||
)
|
||||
|
||||
@@ -150,12 +150,9 @@ class WLEDSegmentLight(WLEDEntity, LightEntity):
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
try:
|
||||
self.coordinator.data.state.segments[self._segment]
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
return super().available
|
||||
return (
|
||||
super().available and self._segment in self.coordinator.data.state.segments
|
||||
)
|
||||
|
||||
@property
|
||||
def rgb_color(self) -> tuple[int, int, int] | None:
|
||||
|
||||
@@ -97,12 +97,9 @@ class WLEDNumber(WLEDEntity, NumberEntity):
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
try:
|
||||
self.coordinator.data.state.segments[self._segment]
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
return super().available
|
||||
return (
|
||||
super().available and self._segment in self.coordinator.data.state.segments
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
|
||||
@@ -31,10 +31,6 @@ rules:
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: done
|
||||
docs-installation-parameters: todo
|
||||
entity-unavailable:
|
||||
status: todo
|
||||
comment: |
|
||||
The WLEDSegmentLight.available property can just be an if .. in .. check
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
|
||||
@@ -173,12 +173,9 @@ class WLEDPaletteSelect(WLEDEntity, SelectEntity):
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
try:
|
||||
self.coordinator.data.state.segments[self._segment]
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
return super().available
|
||||
return (
|
||||
super().available and self._segment in self.coordinator.data.state.segments
|
||||
)
|
||||
|
||||
@property
|
||||
def current_option(self) -> str | None:
|
||||
|
||||
@@ -167,12 +167,9 @@ class WLEDReverseSwitch(WLEDEntity, SwitchEntity):
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
try:
|
||||
self.coordinator.data.state.segments[self._segment]
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
return super().available
|
||||
return (
|
||||
super().available and self._segment in self.coordinator.data.state.segments
|
||||
)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
|
||||
@@ -4832,7 +4832,7 @@
|
||||
},
|
||||
"oralb": {
|
||||
"name": "Oral-B",
|
||||
"integration_type": "hub",
|
||||
"integration_type": "device",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push"
|
||||
},
|
||||
|
||||
4
homeassistant/generated/labs.py
generated
4
homeassistant/generated/labs.py
generated
@@ -6,8 +6,8 @@ To update, run python3 -m script.hassfest
|
||||
LABS_PREVIEW_FEATURES = {
|
||||
"automation": {
|
||||
"new_triggers_conditions": {
|
||||
"feedback_url": "",
|
||||
"learn_more_url": "",
|
||||
"feedback_url": "https://forms.gle/fWFZqf5MzuwWTsCH8",
|
||||
"learn_more_url": "https://www.home-assistant.io/blog/2025/12/03/release-202512/#purpose-specific-triggers-and-conditions",
|
||||
"report_issue_url": "https://github.com/home-assistant/core/issues/new?template=bug_report.yml&integration_link=https://www.home-assistant.io/integrations/automation&integration_name=Automation",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -36,7 +36,7 @@ fnv-hash-fast==1.6.0
|
||||
go2rtc-client==0.3.0
|
||||
ha-ffmpeg==3.2.2
|
||||
habluetooth==5.8.0
|
||||
hass-nabucasa==1.6.2
|
||||
hass-nabucasa==1.7.0
|
||||
hassil==3.5.0
|
||||
home-assistant-bluetooth==1.13.1
|
||||
home-assistant-frontend==20251202.0
|
||||
|
||||
@@ -48,7 +48,7 @@ dependencies = [
|
||||
"fnv-hash-fast==1.6.0",
|
||||
# hass-nabucasa is imported by helpers which don't depend on the cloud
|
||||
# integration
|
||||
"hass-nabucasa==1.6.2",
|
||||
"hass-nabucasa==1.7.0",
|
||||
# When bumping httpx, please check the version pins of
|
||||
# httpcore, anyio, and h11 in gen_requirements_all
|
||||
"httpx==0.28.1",
|
||||
|
||||
2
requirements.txt
generated
2
requirements.txt
generated
@@ -22,7 +22,7 @@ certifi>=2021.5.30
|
||||
ciso8601==2.3.3
|
||||
cronsim==2.7
|
||||
fnv-hash-fast==1.6.0
|
||||
hass-nabucasa==1.6.2
|
||||
hass-nabucasa==1.7.0
|
||||
httpx==0.28.1
|
||||
home-assistant-bluetooth==1.13.1
|
||||
ifaddr==0.2.0
|
||||
|
||||
8
requirements_all.txt
generated
8
requirements_all.txt
generated
@@ -340,7 +340,7 @@ aiontfy==0.6.1
|
||||
aionut==4.3.4
|
||||
|
||||
# homeassistant.components.onkyo
|
||||
aioonkyo==0.3.0
|
||||
aioonkyo==0.4.0
|
||||
|
||||
# homeassistant.components.openexchangerates
|
||||
aioopenexchangerates==0.6.8
|
||||
@@ -1163,7 +1163,7 @@ habluetooth==5.8.0
|
||||
hanna-cloud==0.0.6
|
||||
|
||||
# homeassistant.components.cloud
|
||||
hass-nabucasa==1.6.2
|
||||
hass-nabucasa==1.7.0
|
||||
|
||||
# homeassistant.components.splunk
|
||||
hass-splunk==0.1.1
|
||||
@@ -1288,7 +1288,7 @@ insteon-frontend-home-assistant==0.5.0
|
||||
intellifire4py==4.2.1
|
||||
|
||||
# homeassistant.components.iometer
|
||||
iometer==0.2.0
|
||||
iometer==0.3.0
|
||||
|
||||
# homeassistant.components.iotty
|
||||
iottycloud==0.3.0
|
||||
@@ -3191,7 +3191,7 @@ wyoming==1.7.2
|
||||
xiaomi-ble==1.2.0
|
||||
|
||||
# homeassistant.components.knx
|
||||
xknx==3.11.0
|
||||
xknx==3.12.0
|
||||
|
||||
# homeassistant.components.knx
|
||||
xknxproject==3.8.2
|
||||
|
||||
8
requirements_test_all.txt
generated
8
requirements_test_all.txt
generated
@@ -325,7 +325,7 @@ aiontfy==0.6.1
|
||||
aionut==4.3.4
|
||||
|
||||
# homeassistant.components.onkyo
|
||||
aioonkyo==0.3.0
|
||||
aioonkyo==0.4.0
|
||||
|
||||
# homeassistant.components.openexchangerates
|
||||
aioopenexchangerates==0.6.8
|
||||
@@ -1033,7 +1033,7 @@ habluetooth==5.8.0
|
||||
hanna-cloud==0.0.6
|
||||
|
||||
# homeassistant.components.cloud
|
||||
hass-nabucasa==1.6.2
|
||||
hass-nabucasa==1.7.0
|
||||
|
||||
# homeassistant.components.assist_satellite
|
||||
# homeassistant.components.conversation
|
||||
@@ -1134,7 +1134,7 @@ insteon-frontend-home-assistant==0.5.0
|
||||
intellifire4py==4.2.1
|
||||
|
||||
# homeassistant.components.iometer
|
||||
iometer==0.2.0
|
||||
iometer==0.3.0
|
||||
|
||||
# homeassistant.components.iotty
|
||||
iottycloud==0.3.0
|
||||
@@ -2658,7 +2658,7 @@ wyoming==1.7.2
|
||||
xiaomi-ble==1.2.0
|
||||
|
||||
# homeassistant.components.knx
|
||||
xknx==3.11.0
|
||||
xknx==3.12.0
|
||||
|
||||
# homeassistant.components.knx
|
||||
xknxproject==3.8.2
|
||||
|
||||
@@ -205,8 +205,8 @@ async def test_ws_get_client_config(
|
||||
"iceServers": [
|
||||
{
|
||||
"urls": [
|
||||
"stun:stun.home-assistant.io:80",
|
||||
"stun:stun.home-assistant.io:3478",
|
||||
"stun:stun.home-assistant.io:80",
|
||||
]
|
||||
},
|
||||
],
|
||||
@@ -238,8 +238,8 @@ async def test_ws_get_client_config(
|
||||
"iceServers": [
|
||||
{
|
||||
"urls": [
|
||||
"stun:stun.home-assistant.io:80",
|
||||
"stun:stun.home-assistant.io:3478",
|
||||
"stun:stun.home-assistant.io:80",
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -11,6 +11,7 @@ import pytest
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import conversation
|
||||
from homeassistant.components.cloud.const import AI_TASK_ENTITY_UNIQUE_ID, DOMAIN
|
||||
from homeassistant.components.cloud.entity import (
|
||||
BaseCloudLLMEntity,
|
||||
_convert_content_to_param,
|
||||
@@ -18,7 +19,8 @@ from homeassistant.components.cloud.entity import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import llm, selector
|
||||
from homeassistant.helpers import entity_registry as er, llm, selector
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
@@ -219,3 +221,66 @@ async def test_prepare_chat_for_generation_passes_messages_through(
|
||||
|
||||
assert response["messages"] == messages
|
||||
assert response["conversation_id"] == "conversation-id"
|
||||
|
||||
|
||||
async def test_async_handle_chat_log_service_sets_structured_output_non_strict(
|
||||
hass: HomeAssistant,
|
||||
cloud: MagicMock,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_cloud_login: None,
|
||||
) -> None:
|
||||
"""Ensure structured output requests always disable strict validation via service."""
|
||||
assert await async_setup_component(hass, DOMAIN, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
on_start_callback = cloud.register_on_start.call_args[0][0]
|
||||
await on_start_callback()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = entity_registry.async_get_entity_id(
|
||||
"ai_task", DOMAIN, AI_TASK_ENTITY_UNIQUE_ID
|
||||
)
|
||||
assert entity_id is not None
|
||||
|
||||
async def _empty_stream():
|
||||
return
|
||||
|
||||
async def _fake_delta_stream(
|
||||
self: conversation.ChatLog,
|
||||
agent_id: str,
|
||||
stream,
|
||||
):
|
||||
content = conversation.AssistantContent(
|
||||
agent_id=agent_id, content='{"value": "ok"}'
|
||||
)
|
||||
self.async_add_assistant_content_without_tools(content)
|
||||
yield content
|
||||
|
||||
cloud.llm.async_generate_data = AsyncMock(return_value=_empty_stream())
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.conversation.chat_log.ChatLog.async_add_delta_content_stream",
|
||||
_fake_delta_stream,
|
||||
):
|
||||
await hass.services.async_call(
|
||||
"ai_task",
|
||||
"generate_data",
|
||||
{
|
||||
"entity_id": entity_id,
|
||||
"task_name": "Device Report",
|
||||
"instructions": "Provide value.",
|
||||
"structure": {
|
||||
"value": {
|
||||
"selector": {"text": None},
|
||||
"required": True,
|
||||
}
|
||||
},
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
|
||||
cloud.llm.async_generate_data.assert_awaited_once()
|
||||
_, kwargs = cloud.llm.async_generate_data.call_args
|
||||
|
||||
assert kwargs["response_format"]["json_schema"]["strict"] is False
|
||||
|
||||
@@ -36,6 +36,7 @@ async def test_diagnostic_entities(
|
||||
"sensor.knx_interface_outgoing_telegrams",
|
||||
"sensor.knx_interface_outgoing_telegram_errors",
|
||||
"sensor.knx_interface_telegrams",
|
||||
"sensor.knx_interface_undecodable_data_secure_telegrams",
|
||||
):
|
||||
entity = entity_registry.async_get(entity_id)
|
||||
assert entity.entity_category is EntityCategory.DIAGNOSTIC
|
||||
@@ -43,6 +44,7 @@ async def test_diagnostic_entities(
|
||||
for entity_id in (
|
||||
"sensor.knx_interface_incoming_telegrams",
|
||||
"sensor.knx_interface_outgoing_telegrams",
|
||||
"sensor.knx_interface_undecodable_data_secure_telegrams",
|
||||
):
|
||||
entity = entity_registry.async_get(entity_id)
|
||||
assert entity.disabled is True
|
||||
@@ -57,7 +59,7 @@ async def test_diagnostic_entities(
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(events) == 3 # 5 polled sensors - 2 disabled
|
||||
assert len(events) == 3 # 6 polled sensors - 3 disabled
|
||||
events.clear()
|
||||
|
||||
for entity_id, test_state in (
|
||||
@@ -74,7 +76,7 @@ async def test_diagnostic_entities(
|
||||
state=XknxConnectionState.DISCONNECTED
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert len(events) == 4 # 3 not always_available + 3 force_update - 2 disabled
|
||||
assert len(events) == 4
|
||||
events.clear()
|
||||
|
||||
knx.xknx.current_address = IndividualAddress("1.1.1")
|
||||
|
||||
@@ -4162,81 +4162,3 @@ async def test_get_services_for_target(
|
||||
"switch.turn_on",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@patch("annotatedyaml.loader.load_yaml")
|
||||
@patch.object(Integration, "has_services", return_value=True)
|
||||
async def test_get_services_for_target_caching(
|
||||
mock_has_services: Mock,
|
||||
mock_load_yaml: Mock,
|
||||
hass: HomeAssistant,
|
||||
websocket_client: MockHAClientWebSocket,
|
||||
) -> None:
|
||||
"""Test that flattened service descriptions are cached and reused."""
|
||||
|
||||
def get_common_service_descriptions(domain: str):
|
||||
return f"""
|
||||
turn_on:
|
||||
target:
|
||||
entity:
|
||||
domain: {domain}
|
||||
"""
|
||||
|
||||
def _load_yaml(fname, secrets=None):
|
||||
domain = fname.split("/")[-2]
|
||||
with io.StringIO(get_common_service_descriptions(domain)) as file:
|
||||
return parse_yaml(file)
|
||||
|
||||
mock_load_yaml.side_effect = _load_yaml
|
||||
await hass.async_block_till_done()
|
||||
|
||||
hass.services.async_register("light", "turn_on", lambda call: None)
|
||||
hass.services.async_register("switch", "turn_on", lambda call: None)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
async def call_command():
|
||||
await websocket_client.send_json_auto_id(
|
||||
{
|
||||
"type": "get_services_for_target",
|
||||
"target": {"entity_id": ["light.test1"]},
|
||||
}
|
||||
)
|
||||
msg = await websocket_client.receive_json()
|
||||
assert msg["success"]
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.websocket_api.automation._async_get_automation_components_for_target",
|
||||
return_value=set(),
|
||||
) as mock_get_components:
|
||||
# First call: should create and cache flat descriptions
|
||||
await call_command()
|
||||
|
||||
assert mock_get_components.call_count == 1
|
||||
first_flat_descriptions = mock_get_components.call_args_list[0][0][3]
|
||||
assert first_flat_descriptions == {
|
||||
"light.turn_on": {
|
||||
"fields": {},
|
||||
"target": {"entity": [{"domain": ["light"]}]},
|
||||
},
|
||||
"switch.turn_on": {
|
||||
"fields": {},
|
||||
"target": {"entity": [{"domain": ["switch"]}]},
|
||||
},
|
||||
}
|
||||
|
||||
# Second call: should reuse cached flat descriptions
|
||||
await call_command()
|
||||
assert mock_get_components.call_count == 2
|
||||
second_flat_descriptions = mock_get_components.call_args_list[1][0][3]
|
||||
assert first_flat_descriptions is second_flat_descriptions
|
||||
|
||||
# Register a new service to invalidate cache
|
||||
hass.services.async_register("new_domain", "new_service", lambda call: None)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Third call: cache should be rebuilt
|
||||
await call_command()
|
||||
assert mock_get_components.call_count == 3
|
||||
third_flat_descriptions = mock_get_components.call_args_list[2][0][3]
|
||||
assert "new_domain.new_service" in third_flat_descriptions
|
||||
assert third_flat_descriptions is not first_flat_descriptions
|
||||
|
||||
Reference in New Issue
Block a user