mirror of
https://github.com/home-assistant/core.git
synced 2025-07-25 22:27:07 +00:00
2024.2.1 (#110078)
This commit is contained in:
commit
cfd1f7809f
@ -5,5 +5,5 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/aosmith",
|
"documentation": "https://www.home-assistant.io/integrations/aosmith",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"requirements": ["py-aosmith==1.0.6"]
|
"requirements": ["py-aosmith==1.0.8"]
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
"""Intents for the client integration."""
|
"""Intents for the client integration."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@ -36,24 +37,34 @@ class GetTemperatureIntent(intent.IntentHandler):
|
|||||||
if not entities:
|
if not entities:
|
||||||
raise intent.IntentHandleError("No climate entities")
|
raise intent.IntentHandleError("No climate entities")
|
||||||
|
|
||||||
if "area" in slots:
|
name_slot = slots.get("name", {})
|
||||||
# Filter by area
|
entity_name: str | None = name_slot.get("value")
|
||||||
area_name = slots["area"]["value"]
|
entity_text: str | None = name_slot.get("text")
|
||||||
|
|
||||||
|
area_slot = slots.get("area", {})
|
||||||
|
area_id = area_slot.get("value")
|
||||||
|
|
||||||
|
if area_id:
|
||||||
|
# Filter by area and optionally name
|
||||||
|
area_name = area_slot.get("text")
|
||||||
|
|
||||||
for maybe_climate in intent.async_match_states(
|
for maybe_climate in intent.async_match_states(
|
||||||
hass, area_name=area_name, domains=[DOMAIN]
|
hass, name=entity_name, area_name=area_id, domains=[DOMAIN]
|
||||||
):
|
):
|
||||||
climate_state = maybe_climate
|
climate_state = maybe_climate
|
||||||
break
|
break
|
||||||
|
|
||||||
if climate_state is None:
|
if climate_state is None:
|
||||||
raise intent.IntentHandleError(f"No climate entity in area {area_name}")
|
raise intent.NoStatesMatchedError(
|
||||||
|
name=entity_text or entity_name,
|
||||||
|
area=area_name or area_id,
|
||||||
|
domains={DOMAIN},
|
||||||
|
device_classes=None,
|
||||||
|
)
|
||||||
|
|
||||||
climate_entity = component.get_entity(climate_state.entity_id)
|
climate_entity = component.get_entity(climate_state.entity_id)
|
||||||
elif "name" in slots:
|
elif entity_name:
|
||||||
# Filter by name
|
# Filter by name
|
||||||
entity_name = slots["name"]["value"]
|
|
||||||
|
|
||||||
for maybe_climate in intent.async_match_states(
|
for maybe_climate in intent.async_match_states(
|
||||||
hass, name=entity_name, domains=[DOMAIN]
|
hass, name=entity_name, domains=[DOMAIN]
|
||||||
):
|
):
|
||||||
@ -61,7 +72,12 @@ class GetTemperatureIntent(intent.IntentHandler):
|
|||||||
break
|
break
|
||||||
|
|
||||||
if climate_state is None:
|
if climate_state is None:
|
||||||
raise intent.IntentHandleError(f"No climate entity named {entity_name}")
|
raise intent.NoStatesMatchedError(
|
||||||
|
name=entity_name,
|
||||||
|
area=None,
|
||||||
|
domains={DOMAIN},
|
||||||
|
device_classes=None,
|
||||||
|
)
|
||||||
|
|
||||||
climate_entity = component.get_entity(climate_state.entity_id)
|
climate_entity = component.get_entity(climate_state.entity_id)
|
||||||
else:
|
else:
|
||||||
|
@ -7,5 +7,5 @@
|
|||||||
"integration_type": "service",
|
"integration_type": "service",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["aioelectricitymaps"],
|
"loggers": ["aioelectricitymaps"],
|
||||||
"requirements": ["aioelectricitymaps==0.3.1"]
|
"requirements": ["aioelectricitymaps==0.4.0"]
|
||||||
}
|
}
|
||||||
|
@ -223,22 +223,22 @@ class DefaultAgent(AbstractConversationAgent):
|
|||||||
# Check if a trigger matched
|
# Check if a trigger matched
|
||||||
if isinstance(result, SentenceTriggerResult):
|
if isinstance(result, SentenceTriggerResult):
|
||||||
# Gather callback responses in parallel
|
# Gather callback responses in parallel
|
||||||
trigger_responses = await asyncio.gather(
|
trigger_callbacks = [
|
||||||
*(
|
|
||||||
self._trigger_sentences[trigger_id].callback(
|
self._trigger_sentences[trigger_id].callback(
|
||||||
result.sentence, trigger_result
|
result.sentence, trigger_result
|
||||||
)
|
)
|
||||||
for trigger_id, trigger_result in result.matched_triggers.items()
|
for trigger_id, trigger_result in result.matched_triggers.items()
|
||||||
)
|
]
|
||||||
)
|
|
||||||
|
|
||||||
# Use last non-empty result as response.
|
# Use last non-empty result as response.
|
||||||
#
|
#
|
||||||
# There may be multiple copies of a trigger running when editing in
|
# There may be multiple copies of a trigger running when editing in
|
||||||
# the UI, so it's critical that we filter out empty responses here.
|
# the UI, so it's critical that we filter out empty responses here.
|
||||||
response_text: str | None = None
|
response_text: str | None = None
|
||||||
for trigger_response in trigger_responses:
|
for trigger_future in asyncio.as_completed(trigger_callbacks):
|
||||||
response_text = response_text or trigger_response
|
if trigger_response := await trigger_future:
|
||||||
|
response_text = trigger_response
|
||||||
|
break
|
||||||
|
|
||||||
# Convert to conversation result
|
# Convert to conversation result
|
||||||
response = intent.IntentResponse(language=language)
|
response = intent.IntentResponse(language=language)
|
||||||
@ -316,6 +316,20 @@ class DefaultAgent(AbstractConversationAgent):
|
|||||||
),
|
),
|
||||||
conversation_id,
|
conversation_id,
|
||||||
)
|
)
|
||||||
|
except intent.DuplicateNamesMatchedError as duplicate_names_error:
|
||||||
|
# Intent was valid, but two or more entities with the same name matched.
|
||||||
|
(
|
||||||
|
error_response_type,
|
||||||
|
error_response_args,
|
||||||
|
) = _get_duplicate_names_matched_response(duplicate_names_error)
|
||||||
|
return _make_error_result(
|
||||||
|
language,
|
||||||
|
intent.IntentResponseErrorCode.NO_VALID_TARGETS,
|
||||||
|
self._get_error_text(
|
||||||
|
error_response_type, lang_intents, **error_response_args
|
||||||
|
),
|
||||||
|
conversation_id,
|
||||||
|
)
|
||||||
except intent.IntentHandleError:
|
except intent.IntentHandleError:
|
||||||
# Intent was valid and entities matched constraints, but an error
|
# Intent was valid and entities matched constraints, but an error
|
||||||
# occurred during handling.
|
# occurred during handling.
|
||||||
@ -724,7 +738,12 @@ class DefaultAgent(AbstractConversationAgent):
|
|||||||
if async_should_expose(self.hass, DOMAIN, state.entity_id)
|
if async_should_expose(self.hass, DOMAIN, state.entity_id)
|
||||||
]
|
]
|
||||||
|
|
||||||
# Gather exposed entity names
|
# Gather exposed entity names.
|
||||||
|
#
|
||||||
|
# NOTE: We do not pass entity ids in here because multiple entities may
|
||||||
|
# have the same name. The intent matcher doesn't gather all matching
|
||||||
|
# values for a list, just the first. So we will need to match by name no
|
||||||
|
# matter what.
|
||||||
entity_names = []
|
entity_names = []
|
||||||
for state in states:
|
for state in states:
|
||||||
# Checked against "requires_context" and "excludes_context" in hassil
|
# Checked against "requires_context" and "excludes_context" in hassil
|
||||||
@ -740,7 +759,7 @@ class DefaultAgent(AbstractConversationAgent):
|
|||||||
|
|
||||||
if not entity:
|
if not entity:
|
||||||
# Default name
|
# Default name
|
||||||
entity_names.append((state.name, state.entity_id, context))
|
entity_names.append((state.name, state.name, context))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if entity.aliases:
|
if entity.aliases:
|
||||||
@ -748,12 +767,15 @@ class DefaultAgent(AbstractConversationAgent):
|
|||||||
if not alias.strip():
|
if not alias.strip():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
entity_names.append((alias, state.entity_id, context))
|
entity_names.append((alias, alias, context))
|
||||||
|
|
||||||
# Default name
|
# Default name
|
||||||
entity_names.append((state.name, state.entity_id, context))
|
entity_names.append((state.name, state.name, context))
|
||||||
|
|
||||||
# Expose all areas
|
# Expose all areas.
|
||||||
|
#
|
||||||
|
# We pass in area id here with the expectation that no two areas will
|
||||||
|
# share the same name or alias.
|
||||||
areas = ar.async_get(self.hass)
|
areas = ar.async_get(self.hass)
|
||||||
area_names = []
|
area_names = []
|
||||||
for area in areas.async_list_areas():
|
for area in areas.async_list_areas():
|
||||||
@ -984,6 +1006,20 @@ def _get_no_states_matched_response(
|
|||||||
return ErrorKey.NO_INTENT, {}
|
return ErrorKey.NO_INTENT, {}
|
||||||
|
|
||||||
|
|
||||||
|
def _get_duplicate_names_matched_response(
|
||||||
|
duplicate_names_error: intent.DuplicateNamesMatchedError,
|
||||||
|
) -> tuple[ErrorKey, dict[str, Any]]:
|
||||||
|
"""Return key and template arguments for error when intent returns duplicate matches."""
|
||||||
|
|
||||||
|
if duplicate_names_error.area:
|
||||||
|
return ErrorKey.DUPLICATE_ENTITIES_IN_AREA, {
|
||||||
|
"entity": duplicate_names_error.name,
|
||||||
|
"area": duplicate_names_error.area,
|
||||||
|
}
|
||||||
|
|
||||||
|
return ErrorKey.DUPLICATE_ENTITIES, {"entity": duplicate_names_error.name}
|
||||||
|
|
||||||
|
|
||||||
def _collect_list_references(expression: Expression, list_names: set[str]) -> None:
|
def _collect_list_references(expression: Expression, list_names: set[str]) -> None:
|
||||||
"""Collect list reference names recursively."""
|
"""Collect list reference names recursively."""
|
||||||
if isinstance(expression, Sequence):
|
if isinstance(expression, Sequence):
|
||||||
|
@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||||
"requirements": ["py-sucks==0.9.8", "deebot-client==5.1.0"]
|
"requirements": ["py-sucks==0.9.8", "deebot-client==5.1.1"]
|
||||||
}
|
}
|
||||||
|
@ -6,5 +6,5 @@
|
|||||||
"dependencies": ["webhook"],
|
"dependencies": ["webhook"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/ecowitt",
|
"documentation": "https://www.home-assistant.io/integrations/ecowitt",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"requirements": ["aioecowitt==2024.2.0"]
|
"requirements": ["aioecowitt==2024.2.1"]
|
||||||
}
|
}
|
||||||
|
@ -5,5 +5,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/evohome",
|
"documentation": "https://www.home-assistant.io/integrations/evohome",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["evohomeasync", "evohomeasync2"],
|
"loggers": ["evohomeasync", "evohomeasync2"],
|
||||||
"requirements": ["evohome-async==0.4.17"]
|
"requirements": ["evohome-async==0.4.18"]
|
||||||
}
|
}
|
||||||
|
@ -20,5 +20,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": ["home-assistant-frontend==20240207.0"]
|
"requirements": ["home-assistant-frontend==20240207.1"]
|
||||||
}
|
}
|
||||||
|
@ -7,5 +7,5 @@
|
|||||||
"integration_type": "service",
|
"integration_type": "service",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["aio_geojson_geonetnz_volcano"],
|
"loggers": ["aio_geojson_geonetnz_volcano"],
|
||||||
"requirements": ["aio-geojson-geonetnz-volcano==0.8"]
|
"requirements": ["aio-geojson-geonetnz-volcano==0.9"]
|
||||||
}
|
}
|
||||||
|
@ -506,7 +506,6 @@ class HassIO:
|
|||||||
options = {
|
options = {
|
||||||
"ssl": CONF_SSL_CERTIFICATE in http_config,
|
"ssl": CONF_SSL_CERTIFICATE in http_config,
|
||||||
"port": port,
|
"port": port,
|
||||||
"watchdog": True,
|
|
||||||
"refresh_token": refresh_token.token,
|
"refresh_token": refresh_token.token,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ from typing import Any
|
|||||||
|
|
||||||
from aiohttp import ClientConnectionError
|
from aiohttp import ClientConnectionError
|
||||||
from aiosomecomfort import (
|
from aiosomecomfort import (
|
||||||
|
APIRateLimited,
|
||||||
AuthError,
|
AuthError,
|
||||||
ConnectionError as AscConnectionError,
|
ConnectionError as AscConnectionError,
|
||||||
SomeComfortError,
|
SomeComfortError,
|
||||||
@ -505,10 +506,11 @@ class HoneywellUSThermostat(ClimateEntity):
|
|||||||
await self._device.refresh()
|
await self._device.refresh()
|
||||||
|
|
||||||
except (
|
except (
|
||||||
|
asyncio.TimeoutError,
|
||||||
|
AscConnectionError,
|
||||||
|
APIRateLimited,
|
||||||
AuthError,
|
AuthError,
|
||||||
ClientConnectionError,
|
ClientConnectionError,
|
||||||
AscConnectionError,
|
|
||||||
asyncio.TimeoutError,
|
|
||||||
):
|
):
|
||||||
self._retry += 1
|
self._retry += 1
|
||||||
self._attr_available = self._retry <= RETRY
|
self._attr_available = self._retry <= RETRY
|
||||||
@ -524,7 +526,12 @@ class HoneywellUSThermostat(ClimateEntity):
|
|||||||
await _login()
|
await _login()
|
||||||
return
|
return
|
||||||
|
|
||||||
except (AscConnectionError, ClientConnectionError, asyncio.TimeoutError):
|
except (
|
||||||
|
asyncio.TimeoutError,
|
||||||
|
AscConnectionError,
|
||||||
|
APIRateLimited,
|
||||||
|
ClientConnectionError,
|
||||||
|
):
|
||||||
self._retry += 1
|
self._retry += 1
|
||||||
self._attr_available = self._retry <= RETRY
|
self._attr_available = self._retry <= RETRY
|
||||||
return
|
return
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
"""The Intent integration."""
|
"""The Intent integration."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@ -155,16 +156,18 @@ class GetStateIntentHandler(intent.IntentHandler):
|
|||||||
slots = self.async_validate_slots(intent_obj.slots)
|
slots = self.async_validate_slots(intent_obj.slots)
|
||||||
|
|
||||||
# Entity name to match
|
# Entity name to match
|
||||||
name: str | None = slots.get("name", {}).get("value")
|
name_slot = slots.get("name", {})
|
||||||
|
entity_name: str | None = name_slot.get("value")
|
||||||
|
entity_text: str | None = name_slot.get("text")
|
||||||
|
|
||||||
# Look up area first to fail early
|
# Look up area first to fail early
|
||||||
area_name = slots.get("area", {}).get("value")
|
area_slot = slots.get("area", {})
|
||||||
|
area_id = area_slot.get("value")
|
||||||
|
area_name = area_slot.get("text")
|
||||||
area: ar.AreaEntry | None = None
|
area: ar.AreaEntry | None = None
|
||||||
if area_name is not None:
|
if area_id is not None:
|
||||||
areas = ar.async_get(hass)
|
areas = ar.async_get(hass)
|
||||||
area = areas.async_get_area(area_name) or areas.async_get_area_by_name(
|
area = areas.async_get_area(area_id)
|
||||||
area_name
|
|
||||||
)
|
|
||||||
if area is None:
|
if area is None:
|
||||||
raise intent.IntentHandleError(f"No area named {area_name}")
|
raise intent.IntentHandleError(f"No area named {area_name}")
|
||||||
|
|
||||||
@ -186,7 +189,7 @@ class GetStateIntentHandler(intent.IntentHandler):
|
|||||||
states = list(
|
states = list(
|
||||||
intent.async_match_states(
|
intent.async_match_states(
|
||||||
hass,
|
hass,
|
||||||
name=name,
|
name=entity_name,
|
||||||
area=area,
|
area=area,
|
||||||
domains=domains,
|
domains=domains,
|
||||||
device_classes=device_classes,
|
device_classes=device_classes,
|
||||||
@ -197,13 +200,20 @@ class GetStateIntentHandler(intent.IntentHandler):
|
|||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Found %s state(s) that matched: name=%s, area=%s, domains=%s, device_classes=%s, assistant=%s",
|
"Found %s state(s) that matched: name=%s, area=%s, domains=%s, device_classes=%s, assistant=%s",
|
||||||
len(states),
|
len(states),
|
||||||
name,
|
entity_name,
|
||||||
area,
|
area,
|
||||||
domains,
|
domains,
|
||||||
device_classes,
|
device_classes,
|
||||||
intent_obj.assistant,
|
intent_obj.assistant,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if entity_name and (len(states) > 1):
|
||||||
|
# Multiple entities matched for the same name
|
||||||
|
raise intent.DuplicateNamesMatchedError(
|
||||||
|
name=entity_text or entity_name,
|
||||||
|
area=area_name or area_id,
|
||||||
|
)
|
||||||
|
|
||||||
# Create response
|
# Create response
|
||||||
response = intent_obj.create_response()
|
response = intent_obj.create_response()
|
||||||
response.response_type = intent.IntentResponseType.QUERY_ANSWER
|
response.response_type = intent.IntentResponseType.QUERY_ANSWER
|
||||||
|
@ -16,5 +16,5 @@
|
|||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "assumed_state",
|
"iot_class": "assumed_state",
|
||||||
"loggers": ["keymitt_ble"],
|
"loggers": ["keymitt_ble"],
|
||||||
"requirements": ["PyMicroBot==0.0.10"]
|
"requirements": ["PyMicroBot==0.0.12"]
|
||||||
}
|
}
|
||||||
|
@ -52,11 +52,27 @@ class MatterAdapter:
|
|||||||
|
|
||||||
async def setup_nodes(self) -> None:
|
async def setup_nodes(self) -> None:
|
||||||
"""Set up all existing nodes and subscribe to new nodes."""
|
"""Set up all existing nodes and subscribe to new nodes."""
|
||||||
|
initialized_nodes: set[int] = set()
|
||||||
for node in self.matter_client.get_nodes():
|
for node in self.matter_client.get_nodes():
|
||||||
|
if not node.available:
|
||||||
|
# ignore un-initialized nodes at startup
|
||||||
|
# catch them later when they become available.
|
||||||
|
continue
|
||||||
|
initialized_nodes.add(node.node_id)
|
||||||
self._setup_node(node)
|
self._setup_node(node)
|
||||||
|
|
||||||
def node_added_callback(event: EventType, node: MatterNode) -> None:
|
def node_added_callback(event: EventType, node: MatterNode) -> None:
|
||||||
"""Handle node added event."""
|
"""Handle node added event."""
|
||||||
|
initialized_nodes.add(node.node_id)
|
||||||
|
self._setup_node(node)
|
||||||
|
|
||||||
|
def node_updated_callback(event: EventType, node: MatterNode) -> None:
|
||||||
|
"""Handle node updated event."""
|
||||||
|
if node.node_id in initialized_nodes:
|
||||||
|
return
|
||||||
|
if not node.available:
|
||||||
|
return
|
||||||
|
initialized_nodes.add(node.node_id)
|
||||||
self._setup_node(node)
|
self._setup_node(node)
|
||||||
|
|
||||||
def endpoint_added_callback(event: EventType, data: dict[str, int]) -> None:
|
def endpoint_added_callback(event: EventType, data: dict[str, int]) -> None:
|
||||||
@ -116,6 +132,11 @@ class MatterAdapter:
|
|||||||
callback=node_added_callback, event_filter=EventType.NODE_ADDED
|
callback=node_added_callback, event_filter=EventType.NODE_ADDED
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
self.config_entry.async_on_unload(
|
||||||
|
self.matter_client.subscribe_events(
|
||||||
|
callback=node_updated_callback, event_filter=EventType.NODE_UPDATED
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def _setup_node(self, node: MatterNode) -> None:
|
def _setup_node(self, node: MatterNode) -> None:
|
||||||
"""Set up an node."""
|
"""Set up an node."""
|
||||||
|
@ -129,6 +129,9 @@ class MatterEntity(Entity):
|
|||||||
|
|
||||||
async def async_update(self) -> None:
|
async def async_update(self) -> None:
|
||||||
"""Call when the entity needs to be updated."""
|
"""Call when the entity needs to be updated."""
|
||||||
|
if not self._endpoint.node.available:
|
||||||
|
# skip poll when the node is not (yet) available
|
||||||
|
return
|
||||||
# manually poll/refresh the primary value
|
# manually poll/refresh the primary value
|
||||||
await self.matter_client.refresh_attribute(
|
await self.matter_client.refresh_attribute(
|
||||||
self._endpoint.node.node_id,
|
self._endpoint.node.node_id,
|
||||||
|
@ -6,5 +6,5 @@
|
|||||||
"dependencies": ["websocket_api"],
|
"dependencies": ["websocket_api"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/matter",
|
"documentation": "https://www.home-assistant.io/integrations/matter",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"requirements": ["python-matter-server==5.4.1"]
|
"requirements": ["python-matter-server==5.5.0"]
|
||||||
}
|
}
|
||||||
|
@ -186,7 +186,7 @@ BASE_STRUCT_SCHEMA = BASE_COMPONENT_SCHEMA.extend(
|
|||||||
]
|
]
|
||||||
),
|
),
|
||||||
vol.Optional(CONF_STRUCTURE): cv.string,
|
vol.Optional(CONF_STRUCTURE): cv.string,
|
||||||
vol.Optional(CONF_SCALE, default=1): cv.positive_float,
|
vol.Optional(CONF_SCALE, default=1): vol.Coerce(float),
|
||||||
vol.Optional(CONF_OFFSET, default=0): vol.Coerce(float),
|
vol.Optional(CONF_OFFSET, default=0): vol.Coerce(float),
|
||||||
vol.Optional(CONF_PRECISION): cv.positive_int,
|
vol.Optional(CONF_PRECISION): cv.positive_int,
|
||||||
vol.Optional(
|
vol.Optional(
|
||||||
@ -241,8 +241,8 @@ CLIMATE_SCHEMA = vol.All(
|
|||||||
{
|
{
|
||||||
vol.Required(CONF_TARGET_TEMP): cv.positive_int,
|
vol.Required(CONF_TARGET_TEMP): cv.positive_int,
|
||||||
vol.Optional(CONF_TARGET_TEMP_WRITE_REGISTERS, default=False): cv.boolean,
|
vol.Optional(CONF_TARGET_TEMP_WRITE_REGISTERS, default=False): cv.boolean,
|
||||||
vol.Optional(CONF_MAX_TEMP, default=35): cv.positive_float,
|
vol.Optional(CONF_MAX_TEMP, default=35): vol.Coerce(float),
|
||||||
vol.Optional(CONF_MIN_TEMP, default=5): cv.positive_float,
|
vol.Optional(CONF_MIN_TEMP, default=5): vol.Coerce(float),
|
||||||
vol.Optional(CONF_STEP, default=0.5): vol.Coerce(float),
|
vol.Optional(CONF_STEP, default=0.5): vol.Coerce(float),
|
||||||
vol.Optional(CONF_TEMPERATURE_UNIT, default=DEFAULT_TEMP_UNIT): cv.string,
|
vol.Optional(CONF_TEMPERATURE_UNIT, default=DEFAULT_TEMP_UNIT): cv.string,
|
||||||
vol.Optional(CONF_HVAC_ONOFF_REGISTER): cv.positive_int,
|
vol.Optional(CONF_HVAC_ONOFF_REGISTER): cv.positive_int,
|
||||||
@ -342,8 +342,8 @@ SENSOR_SCHEMA = vol.All(
|
|||||||
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
|
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
|
||||||
vol.Exclusive(CONF_VIRTUAL_COUNT, "vir_sen_count"): cv.positive_int,
|
vol.Exclusive(CONF_VIRTUAL_COUNT, "vir_sen_count"): cv.positive_int,
|
||||||
vol.Exclusive(CONF_SLAVE_COUNT, "vir_sen_count"): cv.positive_int,
|
vol.Exclusive(CONF_SLAVE_COUNT, "vir_sen_count"): cv.positive_int,
|
||||||
vol.Optional(CONF_MIN_VALUE): cv.positive_float,
|
vol.Optional(CONF_MIN_VALUE): vol.Coerce(float),
|
||||||
vol.Optional(CONF_MAX_VALUE): cv.positive_float,
|
vol.Optional(CONF_MAX_VALUE): vol.Coerce(float),
|
||||||
vol.Optional(CONF_NAN_VALUE): nan_validator,
|
vol.Optional(CONF_NAN_VALUE): nan_validator,
|
||||||
vol.Optional(CONF_ZERO_SUPPRESS): cv.positive_float,
|
vol.Optional(CONF_ZERO_SUPPRESS): cv.positive_float,
|
||||||
}
|
}
|
||||||
|
@ -364,7 +364,9 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity):
|
|||||||
|
|
||||||
# Translate the value received
|
# Translate the value received
|
||||||
if fan_mode is not None:
|
if fan_mode is not None:
|
||||||
self._attr_fan_mode = self._fan_mode_mapping_from_modbus[int(fan_mode)]
|
self._attr_fan_mode = self._fan_mode_mapping_from_modbus.get(
|
||||||
|
int(fan_mode), self._attr_fan_mode
|
||||||
|
)
|
||||||
|
|
||||||
# Read the on/off register if defined. If the value in this
|
# Read the on/off register if defined. If the value in this
|
||||||
# register is "OFF", it will take precedence over the value
|
# register is "OFF", it will take precedence over the value
|
||||||
|
@ -7,6 +7,7 @@ from collections.abc import (
|
|||||||
Callable,
|
Callable,
|
||||||
Coroutine,
|
Coroutine,
|
||||||
Generator,
|
Generator,
|
||||||
|
Hashable,
|
||||||
Iterable,
|
Iterable,
|
||||||
Mapping,
|
Mapping,
|
||||||
ValuesView,
|
ValuesView,
|
||||||
@ -49,6 +50,7 @@ from .helpers.event import (
|
|||||||
)
|
)
|
||||||
from .helpers.frame import report
|
from .helpers.frame import report
|
||||||
from .helpers.typing import UNDEFINED, ConfigType, DiscoveryInfoType, UndefinedType
|
from .helpers.typing import UNDEFINED, ConfigType, DiscoveryInfoType, UndefinedType
|
||||||
|
from .loader import async_suggest_report_issue
|
||||||
from .setup import DATA_SETUP_DONE, async_process_deps_reqs, async_setup_component
|
from .setup import DATA_SETUP_DONE, async_process_deps_reqs, async_setup_component
|
||||||
from .util import uuid as uuid_util
|
from .util import uuid as uuid_util
|
||||||
from .util.decorator import Registry
|
from .util.decorator import Registry
|
||||||
@ -1124,9 +1126,10 @@ class ConfigEntryItems(UserDict[str, ConfigEntry]):
|
|||||||
- domain -> unique_id -> ConfigEntry
|
- domain -> unique_id -> ConfigEntry
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self, hass: HomeAssistant) -> None:
|
||||||
"""Initialize the container."""
|
"""Initialize the container."""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
self._hass = hass
|
||||||
self._domain_index: dict[str, list[ConfigEntry]] = {}
|
self._domain_index: dict[str, list[ConfigEntry]] = {}
|
||||||
self._domain_unique_id_index: dict[str, dict[str, ConfigEntry]] = {}
|
self._domain_unique_id_index: dict[str, dict[str, ConfigEntry]] = {}
|
||||||
|
|
||||||
@ -1145,8 +1148,27 @@ class ConfigEntryItems(UserDict[str, ConfigEntry]):
|
|||||||
data[entry_id] = entry
|
data[entry_id] = entry
|
||||||
self._domain_index.setdefault(entry.domain, []).append(entry)
|
self._domain_index.setdefault(entry.domain, []).append(entry)
|
||||||
if entry.unique_id is not None:
|
if entry.unique_id is not None:
|
||||||
|
unique_id_hash = entry.unique_id
|
||||||
|
# Guard against integrations using unhashable unique_id
|
||||||
|
# In HA Core 2024.9, we should remove the guard and instead fail
|
||||||
|
if not isinstance(entry.unique_id, Hashable):
|
||||||
|
unique_id_hash = str(entry.unique_id) # type: ignore[unreachable]
|
||||||
|
report_issue = async_suggest_report_issue(
|
||||||
|
self._hass, integration_domain=entry.domain
|
||||||
|
)
|
||||||
|
_LOGGER.error(
|
||||||
|
(
|
||||||
|
"Config entry '%s' from integration %s has an invalid unique_id"
|
||||||
|
" '%s', please %s"
|
||||||
|
),
|
||||||
|
entry.title,
|
||||||
|
entry.domain,
|
||||||
|
entry.unique_id,
|
||||||
|
report_issue,
|
||||||
|
)
|
||||||
|
|
||||||
self._domain_unique_id_index.setdefault(entry.domain, {})[
|
self._domain_unique_id_index.setdefault(entry.domain, {})[
|
||||||
entry.unique_id
|
unique_id_hash
|
||||||
] = entry
|
] = entry
|
||||||
|
|
||||||
def _unindex_entry(self, entry_id: str) -> None:
|
def _unindex_entry(self, entry_id: str) -> None:
|
||||||
@ -1157,6 +1179,9 @@ class ConfigEntryItems(UserDict[str, ConfigEntry]):
|
|||||||
if not self._domain_index[domain]:
|
if not self._domain_index[domain]:
|
||||||
del self._domain_index[domain]
|
del self._domain_index[domain]
|
||||||
if (unique_id := entry.unique_id) is not None:
|
if (unique_id := entry.unique_id) is not None:
|
||||||
|
# Check type first to avoid expensive isinstance call
|
||||||
|
if type(unique_id) is not str and not isinstance(unique_id, Hashable): # noqa: E721
|
||||||
|
unique_id = str(entry.unique_id) # type: ignore[unreachable]
|
||||||
del self._domain_unique_id_index[domain][unique_id]
|
del self._domain_unique_id_index[domain][unique_id]
|
||||||
if not self._domain_unique_id_index[domain]:
|
if not self._domain_unique_id_index[domain]:
|
||||||
del self._domain_unique_id_index[domain]
|
del self._domain_unique_id_index[domain]
|
||||||
@ -1174,6 +1199,9 @@ class ConfigEntryItems(UserDict[str, ConfigEntry]):
|
|||||||
self, domain: str, unique_id: str
|
self, domain: str, unique_id: str
|
||||||
) -> ConfigEntry | None:
|
) -> ConfigEntry | None:
|
||||||
"""Get entry by domain and unique id."""
|
"""Get entry by domain and unique id."""
|
||||||
|
# Check type first to avoid expensive isinstance call
|
||||||
|
if type(unique_id) is not str and not isinstance(unique_id, Hashable): # noqa: E721
|
||||||
|
unique_id = str(unique_id) # type: ignore[unreachable]
|
||||||
return self._domain_unique_id_index.get(domain, {}).get(unique_id)
|
return self._domain_unique_id_index.get(domain, {}).get(unique_id)
|
||||||
|
|
||||||
|
|
||||||
@ -1189,7 +1217,7 @@ class ConfigEntries:
|
|||||||
self.flow = ConfigEntriesFlowManager(hass, self, hass_config)
|
self.flow = ConfigEntriesFlowManager(hass, self, hass_config)
|
||||||
self.options = OptionsFlowManager(hass)
|
self.options = OptionsFlowManager(hass)
|
||||||
self._hass_config = hass_config
|
self._hass_config = hass_config
|
||||||
self._entries = ConfigEntryItems()
|
self._entries = ConfigEntryItems(hass)
|
||||||
self._store = storage.Store[dict[str, list[dict[str, Any]]]](
|
self._store = storage.Store[dict[str, list[dict[str, Any]]]](
|
||||||
hass, STORAGE_VERSION, STORAGE_KEY
|
hass, STORAGE_VERSION, STORAGE_KEY
|
||||||
)
|
)
|
||||||
@ -1314,10 +1342,10 @@ class ConfigEntries:
|
|||||||
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._async_shutdown)
|
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._async_shutdown)
|
||||||
|
|
||||||
if config is None:
|
if config is None:
|
||||||
self._entries = ConfigEntryItems()
|
self._entries = ConfigEntryItems(self.hass)
|
||||||
return
|
return
|
||||||
|
|
||||||
entries: ConfigEntryItems = ConfigEntryItems()
|
entries: ConfigEntryItems = ConfigEntryItems(self.hass)
|
||||||
for entry in config["entries"]:
|
for entry in config["entries"]:
|
||||||
pref_disable_new_entities = entry.get("pref_disable_new_entities")
|
pref_disable_new_entities = entry.get("pref_disable_new_entities")
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ from .helpers.deprecation import (
|
|||||||
APPLICATION_NAME: Final = "HomeAssistant"
|
APPLICATION_NAME: Final = "HomeAssistant"
|
||||||
MAJOR_VERSION: Final = 2024
|
MAJOR_VERSION: Final = 2024
|
||||||
MINOR_VERSION: Final = 2
|
MINOR_VERSION: Final = 2
|
||||||
PATCH_VERSION: Final = "0"
|
PATCH_VERSION: Final = "1"
|
||||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0)
|
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0)
|
||||||
|
@ -155,6 +155,17 @@ class NoStatesMatchedError(IntentError):
|
|||||||
self.device_classes = device_classes
|
self.device_classes = device_classes
|
||||||
|
|
||||||
|
|
||||||
|
class DuplicateNamesMatchedError(IntentError):
|
||||||
|
"""Error when two or more entities with the same name matched."""
|
||||||
|
|
||||||
|
def __init__(self, name: str, area: str | None) -> None:
|
||||||
|
"""Initialize error."""
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.name = name
|
||||||
|
self.area = area
|
||||||
|
|
||||||
|
|
||||||
def _is_device_class(
|
def _is_device_class(
|
||||||
state: State,
|
state: State,
|
||||||
entity: entity_registry.RegistryEntry | None,
|
entity: entity_registry.RegistryEntry | None,
|
||||||
@ -318,8 +329,6 @@ def async_match_states(
|
|||||||
for state, entity in states_and_entities:
|
for state, entity in states_and_entities:
|
||||||
if _has_name(state, entity, name):
|
if _has_name(state, entity, name):
|
||||||
yield state
|
yield state
|
||||||
break
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Not filtered by name
|
# Not filtered by name
|
||||||
for state, _entity in states_and_entities:
|
for state, _entity in states_and_entities:
|
||||||
@ -403,11 +412,11 @@ class ServiceIntentHandler(IntentHandler):
|
|||||||
slots = self.async_validate_slots(intent_obj.slots)
|
slots = self.async_validate_slots(intent_obj.slots)
|
||||||
|
|
||||||
name_slot = slots.get("name", {})
|
name_slot = slots.get("name", {})
|
||||||
entity_id: str | None = name_slot.get("value")
|
entity_name: str | None = name_slot.get("value")
|
||||||
entity_name: str | None = name_slot.get("text")
|
entity_text: str | None = name_slot.get("text")
|
||||||
if entity_id == "all":
|
if entity_name == "all":
|
||||||
# Don't match on name if targeting all entities
|
# Don't match on name if targeting all entities
|
||||||
entity_id = None
|
entity_name = None
|
||||||
|
|
||||||
# Look up area first to fail early
|
# Look up area first to fail early
|
||||||
area_slot = slots.get("area", {})
|
area_slot = slots.get("area", {})
|
||||||
@ -416,9 +425,7 @@ class ServiceIntentHandler(IntentHandler):
|
|||||||
area: area_registry.AreaEntry | None = None
|
area: area_registry.AreaEntry | None = None
|
||||||
if area_id is not None:
|
if area_id is not None:
|
||||||
areas = area_registry.async_get(hass)
|
areas = area_registry.async_get(hass)
|
||||||
area = areas.async_get_area(area_id) or areas.async_get_area_by_name(
|
area = areas.async_get_area(area_id)
|
||||||
area_name
|
|
||||||
)
|
|
||||||
if area is None:
|
if area is None:
|
||||||
raise IntentHandleError(f"No area named {area_name}")
|
raise IntentHandleError(f"No area named {area_name}")
|
||||||
|
|
||||||
@ -436,7 +443,7 @@ class ServiceIntentHandler(IntentHandler):
|
|||||||
states = list(
|
states = list(
|
||||||
async_match_states(
|
async_match_states(
|
||||||
hass,
|
hass,
|
||||||
name=entity_id,
|
name=entity_name,
|
||||||
area=area,
|
area=area,
|
||||||
domains=domains,
|
domains=domains,
|
||||||
device_classes=device_classes,
|
device_classes=device_classes,
|
||||||
@ -447,14 +454,24 @@ class ServiceIntentHandler(IntentHandler):
|
|||||||
if not states:
|
if not states:
|
||||||
# No states matched constraints
|
# No states matched constraints
|
||||||
raise NoStatesMatchedError(
|
raise NoStatesMatchedError(
|
||||||
name=entity_name or entity_id,
|
name=entity_text or entity_name,
|
||||||
area=area_name or area_id,
|
area=area_name or area_id,
|
||||||
domains=domains,
|
domains=domains,
|
||||||
device_classes=device_classes,
|
device_classes=device_classes,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if entity_name and (len(states) > 1):
|
||||||
|
# Multiple entities matched for the same name
|
||||||
|
raise DuplicateNamesMatchedError(
|
||||||
|
name=entity_text or entity_name,
|
||||||
|
area=area_name or area_id,
|
||||||
|
)
|
||||||
|
|
||||||
response = await self.async_handle_states(intent_obj, states, area)
|
response = await self.async_handle_states(intent_obj, states, area)
|
||||||
|
|
||||||
|
# Make the matched states available in the response
|
||||||
|
response.async_set_states(matched_states=states, unmatched_states=[])
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
async def async_handle_states(
|
async def async_handle_states(
|
||||||
|
@ -273,7 +273,13 @@ class _TranslationCache:
|
|||||||
for key, value in updated_resources.items():
|
for key, value in updated_resources.items():
|
||||||
if key not in cached_resources:
|
if key not in cached_resources:
|
||||||
continue
|
continue
|
||||||
|
try:
|
||||||
tuples = list(string.Formatter().parse(value))
|
tuples = list(string.Formatter().parse(value))
|
||||||
|
except ValueError:
|
||||||
|
_LOGGER.error(
|
||||||
|
("Error while parsing localized (%s) string %s"), language, key
|
||||||
|
)
|
||||||
|
continue
|
||||||
updated_placeholders = {tup[1] for tup in tuples if tup[1] is not None}
|
updated_placeholders = {tup[1] for tup in tuples if tup[1] is not None}
|
||||||
|
|
||||||
tuples = list(string.Formatter().parse(cached_resources[key]))
|
tuples = list(string.Formatter().parse(cached_resources[key]))
|
||||||
|
@ -28,7 +28,7 @@ habluetooth==2.4.0
|
|||||||
hass-nabucasa==0.76.0
|
hass-nabucasa==0.76.0
|
||||||
hassil==1.6.1
|
hassil==1.6.1
|
||||||
home-assistant-bluetooth==1.12.0
|
home-assistant-bluetooth==1.12.0
|
||||||
home-assistant-frontend==20240207.0
|
home-assistant-frontend==20240207.1
|
||||||
home-assistant-intents==2024.2.2
|
home-assistant-intents==2024.2.2
|
||||||
httpx==0.26.0
|
httpx==0.26.0
|
||||||
ifaddr==0.2.0
|
ifaddr==0.2.0
|
||||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "homeassistant"
|
name = "homeassistant"
|
||||||
version = "2024.2.0"
|
version = "2024.2.1"
|
||||||
license = {text = "Apache-2.0"}
|
license = {text = "Apache-2.0"}
|
||||||
description = "Open-source home automation platform running on Python 3."
|
description = "Open-source home automation platform running on Python 3."
|
||||||
readme = "README.rst"
|
readme = "README.rst"
|
||||||
|
@ -76,7 +76,7 @@ PyMetEireann==2021.8.0
|
|||||||
PyMetno==0.11.0
|
PyMetno==0.11.0
|
||||||
|
|
||||||
# homeassistant.components.keymitt_ble
|
# homeassistant.components.keymitt_ble
|
||||||
PyMicroBot==0.0.10
|
PyMicroBot==0.0.12
|
||||||
|
|
||||||
# homeassistant.components.nina
|
# homeassistant.components.nina
|
||||||
PyNINA==0.3.3
|
PyNINA==0.3.3
|
||||||
@ -173,7 +173,7 @@ aio-geojson-generic-client==0.4
|
|||||||
aio-geojson-geonetnz-quakes==0.16
|
aio-geojson-geonetnz-quakes==0.16
|
||||||
|
|
||||||
# homeassistant.components.geonetnz_volcano
|
# homeassistant.components.geonetnz_volcano
|
||||||
aio-geojson-geonetnz-volcano==0.8
|
aio-geojson-geonetnz-volcano==0.9
|
||||||
|
|
||||||
# homeassistant.components.nsw_rural_fire_service_feed
|
# homeassistant.components.nsw_rural_fire_service_feed
|
||||||
aio-geojson-nsw-rfs-incidents==0.7
|
aio-geojson-nsw-rfs-incidents==0.7
|
||||||
@ -230,10 +230,10 @@ aioeafm==0.1.2
|
|||||||
aioeagle==1.1.0
|
aioeagle==1.1.0
|
||||||
|
|
||||||
# homeassistant.components.ecowitt
|
# homeassistant.components.ecowitt
|
||||||
aioecowitt==2024.2.0
|
aioecowitt==2024.2.1
|
||||||
|
|
||||||
# homeassistant.components.co2signal
|
# homeassistant.components.co2signal
|
||||||
aioelectricitymaps==0.3.1
|
aioelectricitymaps==0.4.0
|
||||||
|
|
||||||
# homeassistant.components.emonitor
|
# homeassistant.components.emonitor
|
||||||
aioemonitor==1.0.5
|
aioemonitor==1.0.5
|
||||||
@ -684,7 +684,7 @@ debugpy==1.8.0
|
|||||||
# decora==0.6
|
# decora==0.6
|
||||||
|
|
||||||
# homeassistant.components.ecovacs
|
# homeassistant.components.ecovacs
|
||||||
deebot-client==5.1.0
|
deebot-client==5.1.1
|
||||||
|
|
||||||
# homeassistant.components.ihc
|
# homeassistant.components.ihc
|
||||||
# homeassistant.components.namecheapdns
|
# homeassistant.components.namecheapdns
|
||||||
@ -818,7 +818,7 @@ eufylife-ble-client==0.1.8
|
|||||||
# evdev==1.6.1
|
# evdev==1.6.1
|
||||||
|
|
||||||
# homeassistant.components.evohome
|
# homeassistant.components.evohome
|
||||||
evohome-async==0.4.17
|
evohome-async==0.4.18
|
||||||
|
|
||||||
# homeassistant.components.faa_delays
|
# homeassistant.components.faa_delays
|
||||||
faadelays==2023.9.1
|
faadelays==2023.9.1
|
||||||
@ -1059,7 +1059,7 @@ hole==0.8.0
|
|||||||
holidays==0.42
|
holidays==0.42
|
||||||
|
|
||||||
# homeassistant.components.frontend
|
# homeassistant.components.frontend
|
||||||
home-assistant-frontend==20240207.0
|
home-assistant-frontend==20240207.1
|
||||||
|
|
||||||
# homeassistant.components.conversation
|
# homeassistant.components.conversation
|
||||||
home-assistant-intents==2024.2.2
|
home-assistant-intents==2024.2.2
|
||||||
@ -1579,7 +1579,7 @@ pushover_complete==1.1.1
|
|||||||
pvo==2.1.1
|
pvo==2.1.1
|
||||||
|
|
||||||
# homeassistant.components.aosmith
|
# homeassistant.components.aosmith
|
||||||
py-aosmith==1.0.6
|
py-aosmith==1.0.8
|
||||||
|
|
||||||
# homeassistant.components.canary
|
# homeassistant.components.canary
|
||||||
py-canary==0.5.3
|
py-canary==0.5.3
|
||||||
@ -2238,7 +2238,7 @@ python-kasa[speedups]==0.6.2.1
|
|||||||
# python-lirc==1.2.3
|
# python-lirc==1.2.3
|
||||||
|
|
||||||
# homeassistant.components.matter
|
# homeassistant.components.matter
|
||||||
python-matter-server==5.4.1
|
python-matter-server==5.5.0
|
||||||
|
|
||||||
# homeassistant.components.xiaomi_miio
|
# homeassistant.components.xiaomi_miio
|
||||||
python-miio==0.5.12
|
python-miio==0.5.12
|
||||||
|
@ -64,7 +64,7 @@ PyMetEireann==2021.8.0
|
|||||||
PyMetno==0.11.0
|
PyMetno==0.11.0
|
||||||
|
|
||||||
# homeassistant.components.keymitt_ble
|
# homeassistant.components.keymitt_ble
|
||||||
PyMicroBot==0.0.10
|
PyMicroBot==0.0.12
|
||||||
|
|
||||||
# homeassistant.components.nina
|
# homeassistant.components.nina
|
||||||
PyNINA==0.3.3
|
PyNINA==0.3.3
|
||||||
@ -152,7 +152,7 @@ aio-geojson-generic-client==0.4
|
|||||||
aio-geojson-geonetnz-quakes==0.16
|
aio-geojson-geonetnz-quakes==0.16
|
||||||
|
|
||||||
# homeassistant.components.geonetnz_volcano
|
# homeassistant.components.geonetnz_volcano
|
||||||
aio-geojson-geonetnz-volcano==0.8
|
aio-geojson-geonetnz-volcano==0.9
|
||||||
|
|
||||||
# homeassistant.components.nsw_rural_fire_service_feed
|
# homeassistant.components.nsw_rural_fire_service_feed
|
||||||
aio-geojson-nsw-rfs-incidents==0.7
|
aio-geojson-nsw-rfs-incidents==0.7
|
||||||
@ -209,10 +209,10 @@ aioeafm==0.1.2
|
|||||||
aioeagle==1.1.0
|
aioeagle==1.1.0
|
||||||
|
|
||||||
# homeassistant.components.ecowitt
|
# homeassistant.components.ecowitt
|
||||||
aioecowitt==2024.2.0
|
aioecowitt==2024.2.1
|
||||||
|
|
||||||
# homeassistant.components.co2signal
|
# homeassistant.components.co2signal
|
||||||
aioelectricitymaps==0.3.1
|
aioelectricitymaps==0.4.0
|
||||||
|
|
||||||
# homeassistant.components.emonitor
|
# homeassistant.components.emonitor
|
||||||
aioemonitor==1.0.5
|
aioemonitor==1.0.5
|
||||||
@ -559,7 +559,7 @@ dbus-fast==2.21.1
|
|||||||
debugpy==1.8.0
|
debugpy==1.8.0
|
||||||
|
|
||||||
# homeassistant.components.ecovacs
|
# homeassistant.components.ecovacs
|
||||||
deebot-client==5.1.0
|
deebot-client==5.1.1
|
||||||
|
|
||||||
# homeassistant.components.ihc
|
# homeassistant.components.ihc
|
||||||
# homeassistant.components.namecheapdns
|
# homeassistant.components.namecheapdns
|
||||||
@ -855,7 +855,7 @@ hole==0.8.0
|
|||||||
holidays==0.42
|
holidays==0.42
|
||||||
|
|
||||||
# homeassistant.components.frontend
|
# homeassistant.components.frontend
|
||||||
home-assistant-frontend==20240207.0
|
home-assistant-frontend==20240207.1
|
||||||
|
|
||||||
# homeassistant.components.conversation
|
# homeassistant.components.conversation
|
||||||
home-assistant-intents==2024.2.2
|
home-assistant-intents==2024.2.2
|
||||||
@ -1232,7 +1232,7 @@ pushover_complete==1.1.1
|
|||||||
pvo==2.1.1
|
pvo==2.1.1
|
||||||
|
|
||||||
# homeassistant.components.aosmith
|
# homeassistant.components.aosmith
|
||||||
py-aosmith==1.0.6
|
py-aosmith==1.0.8
|
||||||
|
|
||||||
# homeassistant.components.canary
|
# homeassistant.components.canary
|
||||||
py-canary==0.5.3
|
py-canary==0.5.3
|
||||||
@ -1711,7 +1711,7 @@ python-juicenet==1.1.0
|
|||||||
python-kasa[speedups]==0.6.2.1
|
python-kasa[speedups]==0.6.2.1
|
||||||
|
|
||||||
# homeassistant.components.matter
|
# homeassistant.components.matter
|
||||||
python-matter-server==5.4.1
|
python-matter-server==5.5.0
|
||||||
|
|
||||||
# homeassistant.components.xiaomi_miio
|
# homeassistant.components.xiaomi_miio
|
||||||
python-miio==0.5.12
|
python-miio==0.5.12
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
"""Test climate intents."""
|
"""Test climate intents."""
|
||||||
|
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
@ -135,8 +136,10 @@ async def test_get_temperature(
|
|||||||
# Add climate entities to different areas:
|
# Add climate entities to different areas:
|
||||||
# climate_1 => living room
|
# climate_1 => living room
|
||||||
# climate_2 => bedroom
|
# climate_2 => bedroom
|
||||||
|
# nothing in office
|
||||||
living_room_area = area_registry.async_create(name="Living Room")
|
living_room_area = area_registry.async_create(name="Living Room")
|
||||||
bedroom_area = area_registry.async_create(name="Bedroom")
|
bedroom_area = area_registry.async_create(name="Bedroom")
|
||||||
|
office_area = area_registry.async_create(name="Office")
|
||||||
|
|
||||||
entity_registry.async_update_entity(
|
entity_registry.async_update_entity(
|
||||||
climate_1.entity_id, area_id=living_room_area.id
|
climate_1.entity_id, area_id=living_room_area.id
|
||||||
@ -158,7 +161,7 @@ async def test_get_temperature(
|
|||||||
hass,
|
hass,
|
||||||
"test",
|
"test",
|
||||||
climate_intent.INTENT_GET_TEMPERATURE,
|
climate_intent.INTENT_GET_TEMPERATURE,
|
||||||
{"area": {"value": "Bedroom"}},
|
{"area": {"value": bedroom_area.name}},
|
||||||
)
|
)
|
||||||
assert response.response_type == intent.IntentResponseType.QUERY_ANSWER
|
assert response.response_type == intent.IntentResponseType.QUERY_ANSWER
|
||||||
assert len(response.matched_states) == 1
|
assert len(response.matched_states) == 1
|
||||||
@ -179,6 +182,52 @@ async def test_get_temperature(
|
|||||||
state = response.matched_states[0]
|
state = response.matched_states[0]
|
||||||
assert state.attributes["current_temperature"] == 22.0
|
assert state.attributes["current_temperature"] == 22.0
|
||||||
|
|
||||||
|
# Check area with no climate entities
|
||||||
|
with pytest.raises(intent.NoStatesMatchedError) as error:
|
||||||
|
response = await intent.async_handle(
|
||||||
|
hass,
|
||||||
|
"test",
|
||||||
|
climate_intent.INTENT_GET_TEMPERATURE,
|
||||||
|
{"area": {"value": office_area.name}},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Exception should contain details of what we tried to match
|
||||||
|
assert isinstance(error.value, intent.NoStatesMatchedError)
|
||||||
|
assert error.value.name is None
|
||||||
|
assert error.value.area == office_area.name
|
||||||
|
assert error.value.domains == {DOMAIN}
|
||||||
|
assert error.value.device_classes is None
|
||||||
|
|
||||||
|
# Check wrong name
|
||||||
|
with pytest.raises(intent.NoStatesMatchedError) as error:
|
||||||
|
response = await intent.async_handle(
|
||||||
|
hass,
|
||||||
|
"test",
|
||||||
|
climate_intent.INTENT_GET_TEMPERATURE,
|
||||||
|
{"name": {"value": "Does not exist"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert isinstance(error.value, intent.NoStatesMatchedError)
|
||||||
|
assert error.value.name == "Does not exist"
|
||||||
|
assert error.value.area is None
|
||||||
|
assert error.value.domains == {DOMAIN}
|
||||||
|
assert error.value.device_classes is None
|
||||||
|
|
||||||
|
# Check wrong name with area
|
||||||
|
with pytest.raises(intent.NoStatesMatchedError) as error:
|
||||||
|
response = await intent.async_handle(
|
||||||
|
hass,
|
||||||
|
"test",
|
||||||
|
climate_intent.INTENT_GET_TEMPERATURE,
|
||||||
|
{"name": {"value": "Climate 1"}, "area": {"value": bedroom_area.name}},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert isinstance(error.value, intent.NoStatesMatchedError)
|
||||||
|
assert error.value.name == "Climate 1"
|
||||||
|
assert error.value.area == bedroom_area.name
|
||||||
|
assert error.value.domains == {DOMAIN}
|
||||||
|
assert error.value.device_classes is None
|
||||||
|
|
||||||
|
|
||||||
async def test_get_temperature_no_entities(
|
async def test_get_temperature_no_entities(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@ -216,19 +265,28 @@ async def test_get_temperature_no_state(
|
|||||||
climate_1.entity_id, area_id=living_room_area.id
|
climate_1.entity_id, area_id=living_room_area.id
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch("homeassistant.core.StateMachine.get", return_value=None), pytest.raises(
|
with (
|
||||||
intent.IntentHandleError
|
patch("homeassistant.core.StateMachine.get", return_value=None),
|
||||||
|
pytest.raises(intent.IntentHandleError),
|
||||||
):
|
):
|
||||||
await intent.async_handle(
|
await intent.async_handle(
|
||||||
hass, "test", climate_intent.INTENT_GET_TEMPERATURE, {}
|
hass, "test", climate_intent.INTENT_GET_TEMPERATURE, {}
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch(
|
with (
|
||||||
"homeassistant.core.StateMachine.async_all", return_value=[]
|
patch("homeassistant.core.StateMachine.async_all", return_value=[]),
|
||||||
), pytest.raises(intent.IntentHandleError):
|
pytest.raises(intent.NoStatesMatchedError) as error,
|
||||||
|
):
|
||||||
await intent.async_handle(
|
await intent.async_handle(
|
||||||
hass,
|
hass,
|
||||||
"test",
|
"test",
|
||||||
climate_intent.INTENT_GET_TEMPERATURE,
|
climate_intent.INTENT_GET_TEMPERATURE,
|
||||||
{"area": {"value": "Living Room"}},
|
{"area": {"value": "Living Room"}},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Exception should contain details of what we tried to match
|
||||||
|
assert isinstance(error.value, intent.NoStatesMatchedError)
|
||||||
|
assert error.value.name is None
|
||||||
|
assert error.value.area == "Living Room"
|
||||||
|
assert error.value.domains == {DOMAIN}
|
||||||
|
assert error.value.device_classes is None
|
||||||
|
@ -1397,7 +1397,7 @@
|
|||||||
'name': dict({
|
'name': dict({
|
||||||
'name': 'name',
|
'name': 'name',
|
||||||
'text': 'my cool light',
|
'text': 'my cool light',
|
||||||
'value': 'light.kitchen',
|
'value': 'my cool light',
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
'intent': dict({
|
'intent': dict({
|
||||||
@ -1422,7 +1422,7 @@
|
|||||||
'name': dict({
|
'name': dict({
|
||||||
'name': 'name',
|
'name': 'name',
|
||||||
'text': 'my cool light',
|
'text': 'my cool light',
|
||||||
'value': 'light.kitchen',
|
'value': 'my cool light',
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
'intent': dict({
|
'intent': dict({
|
||||||
@ -1572,7 +1572,7 @@
|
|||||||
'name': dict({
|
'name': dict({
|
||||||
'name': 'name',
|
'name': 'name',
|
||||||
'text': 'test light',
|
'text': 'test light',
|
||||||
'value': 'light.demo_1234',
|
'value': 'test light',
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
'intent': dict({
|
'intent': dict({
|
||||||
@ -1604,7 +1604,7 @@
|
|||||||
'name': dict({
|
'name': dict({
|
||||||
'name': 'name',
|
'name': 'name',
|
||||||
'text': 'test light',
|
'text': 'test light',
|
||||||
'value': 'light.demo_1234',
|
'value': 'test light',
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
'intent': dict({
|
'intent': dict({
|
||||||
|
@ -101,7 +101,7 @@ async def test_exposed_areas(
|
|||||||
device_registry.async_update_device(kitchen_device.id, area_id=area_kitchen.id)
|
device_registry.async_update_device(kitchen_device.id, area_id=area_kitchen.id)
|
||||||
|
|
||||||
kitchen_light = entity_registry.async_get_or_create("light", "demo", "1234")
|
kitchen_light = entity_registry.async_get_or_create("light", "demo", "1234")
|
||||||
entity_registry.async_update_entity(
|
kitchen_light = entity_registry.async_update_entity(
|
||||||
kitchen_light.entity_id, device_id=kitchen_device.id
|
kitchen_light.entity_id, device_id=kitchen_device.id
|
||||||
)
|
)
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
@ -109,7 +109,7 @@ async def test_exposed_areas(
|
|||||||
)
|
)
|
||||||
|
|
||||||
bedroom_light = entity_registry.async_get_or_create("light", "demo", "5678")
|
bedroom_light = entity_registry.async_get_or_create("light", "demo", "5678")
|
||||||
entity_registry.async_update_entity(
|
bedroom_light = entity_registry.async_update_entity(
|
||||||
bedroom_light.entity_id, area_id=area_bedroom.id
|
bedroom_light.entity_id, area_id=area_bedroom.id
|
||||||
)
|
)
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
@ -206,14 +206,14 @@ async def test_unexposed_entities_skipped(
|
|||||||
|
|
||||||
# Both lights are in the kitchen
|
# Both lights are in the kitchen
|
||||||
exposed_light = entity_registry.async_get_or_create("light", "demo", "1234")
|
exposed_light = entity_registry.async_get_or_create("light", "demo", "1234")
|
||||||
entity_registry.async_update_entity(
|
exposed_light = entity_registry.async_update_entity(
|
||||||
exposed_light.entity_id,
|
exposed_light.entity_id,
|
||||||
area_id=area_kitchen.id,
|
area_id=area_kitchen.id,
|
||||||
)
|
)
|
||||||
hass.states.async_set(exposed_light.entity_id, "off")
|
hass.states.async_set(exposed_light.entity_id, "off")
|
||||||
|
|
||||||
unexposed_light = entity_registry.async_get_or_create("light", "demo", "5678")
|
unexposed_light = entity_registry.async_get_or_create("light", "demo", "5678")
|
||||||
entity_registry.async_update_entity(
|
unexposed_light = entity_registry.async_update_entity(
|
||||||
unexposed_light.entity_id,
|
unexposed_light.entity_id,
|
||||||
area_id=area_kitchen.id,
|
area_id=area_kitchen.id,
|
||||||
)
|
)
|
||||||
@ -336,7 +336,9 @@ async def test_device_area_context(
|
|||||||
light_entity = entity_registry.async_get_or_create(
|
light_entity = entity_registry.async_get_or_create(
|
||||||
"light", "demo", f"{area.name}-light-{i}"
|
"light", "demo", f"{area.name}-light-{i}"
|
||||||
)
|
)
|
||||||
entity_registry.async_update_entity(light_entity.entity_id, area_id=area.id)
|
light_entity = entity_registry.async_update_entity(
|
||||||
|
light_entity.entity_id, area_id=area.id
|
||||||
|
)
|
||||||
hass.states.async_set(
|
hass.states.async_set(
|
||||||
light_entity.entity_id,
|
light_entity.entity_id,
|
||||||
"off",
|
"off",
|
||||||
@ -612,6 +614,115 @@ async def test_error_no_intent(hass: HomeAssistant, init_components) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_error_duplicate_names(
|
||||||
|
hass: HomeAssistant, init_components, entity_registry: er.EntityRegistry
|
||||||
|
) -> None:
|
||||||
|
"""Test error message when multiple devices have the same name (or alias)."""
|
||||||
|
kitchen_light_1 = entity_registry.async_get_or_create("light", "demo", "1234")
|
||||||
|
kitchen_light_2 = entity_registry.async_get_or_create("light", "demo", "5678")
|
||||||
|
|
||||||
|
# Same name and alias
|
||||||
|
for light in (kitchen_light_1, kitchen_light_2):
|
||||||
|
light = entity_registry.async_update_entity(
|
||||||
|
light.entity_id,
|
||||||
|
name="kitchen light",
|
||||||
|
aliases={"overhead light"},
|
||||||
|
)
|
||||||
|
hass.states.async_set(
|
||||||
|
light.entity_id,
|
||||||
|
"off",
|
||||||
|
attributes={ATTR_FRIENDLY_NAME: light.name},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check name and alias
|
||||||
|
for name in ("kitchen light", "overhead light"):
|
||||||
|
# command
|
||||||
|
result = await conversation.async_converse(
|
||||||
|
hass, f"turn on {name}", None, Context(), None
|
||||||
|
)
|
||||||
|
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||||
|
assert (
|
||||||
|
result.response.error_code
|
||||||
|
== intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
result.response.speech["plain"]["speech"]
|
||||||
|
== f"Sorry, there are multiple devices called {name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# question
|
||||||
|
result = await conversation.async_converse(
|
||||||
|
hass, f"is {name} on?", None, Context(), None
|
||||||
|
)
|
||||||
|
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||||
|
assert (
|
||||||
|
result.response.error_code
|
||||||
|
== intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
result.response.speech["plain"]["speech"]
|
||||||
|
== f"Sorry, there are multiple devices called {name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_error_duplicate_names_in_area(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
init_components,
|
||||||
|
area_registry: ar.AreaRegistry,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
) -> None:
|
||||||
|
"""Test error message when multiple devices have the same name (or alias)."""
|
||||||
|
area_kitchen = area_registry.async_get_or_create("kitchen_id")
|
||||||
|
area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen")
|
||||||
|
|
||||||
|
kitchen_light_1 = entity_registry.async_get_or_create("light", "demo", "1234")
|
||||||
|
kitchen_light_2 = entity_registry.async_get_or_create("light", "demo", "5678")
|
||||||
|
|
||||||
|
# Same name and alias
|
||||||
|
for light in (kitchen_light_1, kitchen_light_2):
|
||||||
|
light = entity_registry.async_update_entity(
|
||||||
|
light.entity_id,
|
||||||
|
name="kitchen light",
|
||||||
|
area_id=area_kitchen.id,
|
||||||
|
aliases={"overhead light"},
|
||||||
|
)
|
||||||
|
hass.states.async_set(
|
||||||
|
light.entity_id,
|
||||||
|
"off",
|
||||||
|
attributes={ATTR_FRIENDLY_NAME: light.name},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check name and alias
|
||||||
|
for name in ("kitchen light", "overhead light"):
|
||||||
|
# command
|
||||||
|
result = await conversation.async_converse(
|
||||||
|
hass, f"turn on {name} in {area_kitchen.name}", None, Context(), None
|
||||||
|
)
|
||||||
|
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||||
|
assert (
|
||||||
|
result.response.error_code
|
||||||
|
== intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
result.response.speech["plain"]["speech"]
|
||||||
|
== f"Sorry, there are multiple devices called {name} in the {area_kitchen.name} area"
|
||||||
|
)
|
||||||
|
|
||||||
|
# question
|
||||||
|
result = await conversation.async_converse(
|
||||||
|
hass, f"is {name} on in the {area_kitchen.name}?", None, Context(), None
|
||||||
|
)
|
||||||
|
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||||
|
assert (
|
||||||
|
result.response.error_code
|
||||||
|
== intent.IntentResponseErrorCode.NO_VALID_TARGETS
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
result.response.speech["plain"]["speech"]
|
||||||
|
== f"Sorry, there are multiple devices called {name} in the {area_kitchen.name} area"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_no_states_matched_default_error(
|
async def test_no_states_matched_default_error(
|
||||||
hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry
|
hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -692,7 +803,7 @@ async def test_empty_aliases(
|
|||||||
|
|
||||||
names = slot_lists["name"]
|
names = slot_lists["name"]
|
||||||
assert len(names.values) == 1
|
assert len(names.values) == 1
|
||||||
assert names.values[0].value_out == kitchen_light.entity_id
|
assert names.values[0].value_out == kitchen_light.name
|
||||||
assert names.values[0].text_in.text == kitchen_light.name
|
assert names.values[0].text_in.text == kitchen_light.name
|
||||||
|
|
||||||
|
|
||||||
@ -713,3 +824,191 @@ async def test_all_domains_loaded(hass: HomeAssistant, init_components) -> None:
|
|||||||
result.response.speech["plain"]["speech"]
|
result.response.speech["plain"]["speech"]
|
||||||
== "Sorry, I am not aware of any device called test light"
|
== "Sorry, I am not aware of any device called test light"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_same_named_entities_in_different_areas(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
init_components,
|
||||||
|
area_registry: ar.AreaRegistry,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
) -> None:
|
||||||
|
"""Test that entities with the same name in different areas can be targeted."""
|
||||||
|
area_kitchen = area_registry.async_get_or_create("kitchen_id")
|
||||||
|
area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen")
|
||||||
|
|
||||||
|
area_bedroom = area_registry.async_get_or_create("bedroom_id")
|
||||||
|
area_bedroom = area_registry.async_update(area_bedroom.id, name="bedroom")
|
||||||
|
|
||||||
|
# Both lights have the same name, but are in different areas
|
||||||
|
kitchen_light = entity_registry.async_get_or_create("light", "demo", "1234")
|
||||||
|
kitchen_light = entity_registry.async_update_entity(
|
||||||
|
kitchen_light.entity_id,
|
||||||
|
area_id=area_kitchen.id,
|
||||||
|
name="overhead light",
|
||||||
|
)
|
||||||
|
hass.states.async_set(
|
||||||
|
kitchen_light.entity_id,
|
||||||
|
"off",
|
||||||
|
attributes={ATTR_FRIENDLY_NAME: kitchen_light.name},
|
||||||
|
)
|
||||||
|
|
||||||
|
bedroom_light = entity_registry.async_get_or_create("light", "demo", "5678")
|
||||||
|
bedroom_light = entity_registry.async_update_entity(
|
||||||
|
bedroom_light.entity_id,
|
||||||
|
area_id=area_bedroom.id,
|
||||||
|
name="overhead light",
|
||||||
|
)
|
||||||
|
hass.states.async_set(
|
||||||
|
bedroom_light.entity_id,
|
||||||
|
"off",
|
||||||
|
attributes={ATTR_FRIENDLY_NAME: bedroom_light.name},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Target kitchen light
|
||||||
|
calls = async_mock_service(hass, "light", "turn_on")
|
||||||
|
result = await conversation.async_converse(
|
||||||
|
hass, "turn on overhead light in the kitchen", None, Context(), None
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(calls) == 1
|
||||||
|
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||||
|
assert result.response.intent is not None
|
||||||
|
assert (
|
||||||
|
result.response.intent.slots.get("name", {}).get("value") == kitchen_light.name
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
result.response.intent.slots.get("name", {}).get("text") == kitchen_light.name
|
||||||
|
)
|
||||||
|
assert len(result.response.matched_states) == 1
|
||||||
|
assert result.response.matched_states[0].entity_id == kitchen_light.entity_id
|
||||||
|
assert calls[0].data.get("entity_id") == [kitchen_light.entity_id]
|
||||||
|
|
||||||
|
# Target bedroom light
|
||||||
|
calls.clear()
|
||||||
|
result = await conversation.async_converse(
|
||||||
|
hass, "turn on overhead light in the bedroom", None, Context(), None
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(calls) == 1
|
||||||
|
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||||
|
assert result.response.intent is not None
|
||||||
|
assert (
|
||||||
|
result.response.intent.slots.get("name", {}).get("value") == bedroom_light.name
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
result.response.intent.slots.get("name", {}).get("text") == bedroom_light.name
|
||||||
|
)
|
||||||
|
assert len(result.response.matched_states) == 1
|
||||||
|
assert result.response.matched_states[0].entity_id == bedroom_light.entity_id
|
||||||
|
assert calls[0].data.get("entity_id") == [bedroom_light.entity_id]
|
||||||
|
|
||||||
|
# Targeting a duplicate name should fail
|
||||||
|
result = await conversation.async_converse(
|
||||||
|
hass, "turn on overhead light", None, Context(), None
|
||||||
|
)
|
||||||
|
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||||
|
|
||||||
|
# Querying a duplicate name should also fail
|
||||||
|
result = await conversation.async_converse(
|
||||||
|
hass, "is the overhead light on?", None, Context(), None
|
||||||
|
)
|
||||||
|
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||||
|
|
||||||
|
# But we can still ask questions that don't rely on the name
|
||||||
|
result = await conversation.async_converse(
|
||||||
|
hass, "how many lights are on?", None, Context(), None
|
||||||
|
)
|
||||||
|
assert result.response.response_type == intent.IntentResponseType.QUERY_ANSWER
|
||||||
|
|
||||||
|
|
||||||
|
async def test_same_aliased_entities_in_different_areas(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
init_components,
|
||||||
|
area_registry: ar.AreaRegistry,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
) -> None:
|
||||||
|
"""Test that entities with the same alias (but different names) in different areas can be targeted."""
|
||||||
|
area_kitchen = area_registry.async_get_or_create("kitchen_id")
|
||||||
|
area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen")
|
||||||
|
|
||||||
|
area_bedroom = area_registry.async_get_or_create("bedroom_id")
|
||||||
|
area_bedroom = area_registry.async_update(area_bedroom.id, name="bedroom")
|
||||||
|
|
||||||
|
# Both lights have the same alias, but are in different areas
|
||||||
|
kitchen_light = entity_registry.async_get_or_create("light", "demo", "1234")
|
||||||
|
kitchen_light = entity_registry.async_update_entity(
|
||||||
|
kitchen_light.entity_id,
|
||||||
|
area_id=area_kitchen.id,
|
||||||
|
name="kitchen overhead light",
|
||||||
|
aliases={"overhead light"},
|
||||||
|
)
|
||||||
|
hass.states.async_set(
|
||||||
|
kitchen_light.entity_id,
|
||||||
|
"off",
|
||||||
|
attributes={ATTR_FRIENDLY_NAME: kitchen_light.name},
|
||||||
|
)
|
||||||
|
|
||||||
|
bedroom_light = entity_registry.async_get_or_create("light", "demo", "5678")
|
||||||
|
bedroom_light = entity_registry.async_update_entity(
|
||||||
|
bedroom_light.entity_id,
|
||||||
|
area_id=area_bedroom.id,
|
||||||
|
name="bedroom overhead light",
|
||||||
|
aliases={"overhead light"},
|
||||||
|
)
|
||||||
|
hass.states.async_set(
|
||||||
|
bedroom_light.entity_id,
|
||||||
|
"off",
|
||||||
|
attributes={ATTR_FRIENDLY_NAME: bedroom_light.name},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Target kitchen light
|
||||||
|
calls = async_mock_service(hass, "light", "turn_on")
|
||||||
|
result = await conversation.async_converse(
|
||||||
|
hass, "turn on overhead light in the kitchen", None, Context(), None
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(calls) == 1
|
||||||
|
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||||
|
assert result.response.intent is not None
|
||||||
|
assert result.response.intent.slots.get("name", {}).get("value") == "overhead light"
|
||||||
|
assert result.response.intent.slots.get("name", {}).get("text") == "overhead light"
|
||||||
|
assert len(result.response.matched_states) == 1
|
||||||
|
assert result.response.matched_states[0].entity_id == kitchen_light.entity_id
|
||||||
|
assert calls[0].data.get("entity_id") == [kitchen_light.entity_id]
|
||||||
|
|
||||||
|
# Target bedroom light
|
||||||
|
calls.clear()
|
||||||
|
result = await conversation.async_converse(
|
||||||
|
hass, "turn on overhead light in the bedroom", None, Context(), None
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(calls) == 1
|
||||||
|
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
|
||||||
|
assert result.response.intent is not None
|
||||||
|
assert result.response.intent.slots.get("name", {}).get("value") == "overhead light"
|
||||||
|
assert result.response.intent.slots.get("name", {}).get("text") == "overhead light"
|
||||||
|
assert len(result.response.matched_states) == 1
|
||||||
|
assert result.response.matched_states[0].entity_id == bedroom_light.entity_id
|
||||||
|
assert calls[0].data.get("entity_id") == [bedroom_light.entity_id]
|
||||||
|
|
||||||
|
# Targeting a duplicate alias should fail
|
||||||
|
result = await conversation.async_converse(
|
||||||
|
hass, "turn on overhead light", None, Context(), None
|
||||||
|
)
|
||||||
|
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||||
|
|
||||||
|
# Querying a duplicate alias should also fail
|
||||||
|
result = await conversation.async_converse(
|
||||||
|
hass, "is the overhead light on?", None, Context(), None
|
||||||
|
)
|
||||||
|
assert result.response.response_type == intent.IntentResponseType.ERROR
|
||||||
|
|
||||||
|
# But we can still ask questions that don't rely on the alias
|
||||||
|
result = await conversation.async_converse(
|
||||||
|
hass, "how many lights are on?", None, Context(), None
|
||||||
|
)
|
||||||
|
assert result.response.response_type == intent.IntentResponseType.QUERY_ANSWER
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
"""Test conversation triggers."""
|
"""Test conversation triggers."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
@ -70,7 +73,7 @@ async def test_if_fires_on_event(hass: HomeAssistant, calls, setup_comp) -> None
|
|||||||
|
|
||||||
|
|
||||||
async def test_response(hass: HomeAssistant, setup_comp) -> None:
|
async def test_response(hass: HomeAssistant, setup_comp) -> None:
|
||||||
"""Test the firing of events."""
|
"""Test the conversation response action."""
|
||||||
response = "I'm sorry, Dave. I'm afraid I can't do that"
|
response = "I'm sorry, Dave. I'm afraid I can't do that"
|
||||||
assert await async_setup_component(
|
assert await async_setup_component(
|
||||||
hass,
|
hass,
|
||||||
@ -100,6 +103,116 @@ async def test_response(hass: HomeAssistant, setup_comp) -> None:
|
|||||||
assert service_response["response"]["speech"]["plain"]["speech"] == response
|
assert service_response["response"]["speech"]["plain"]["speech"] == response
|
||||||
|
|
||||||
|
|
||||||
|
async def test_response_same_sentence(hass: HomeAssistant, calls, setup_comp) -> None:
|
||||||
|
"""Test the conversation response action with multiple triggers using the same sentence."""
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
"automation",
|
||||||
|
{
|
||||||
|
"automation": [
|
||||||
|
{
|
||||||
|
"trigger": {
|
||||||
|
"id": "trigger1",
|
||||||
|
"platform": "conversation",
|
||||||
|
"command": ["test sentence"],
|
||||||
|
},
|
||||||
|
"action": [
|
||||||
|
# Add delay so this response will not be the first
|
||||||
|
{"delay": "0:0:0.100"},
|
||||||
|
{
|
||||||
|
"service": "test.automation",
|
||||||
|
"data_template": {"data": "{{ trigger }}"},
|
||||||
|
},
|
||||||
|
{"set_conversation_response": "response 2"},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"trigger": {
|
||||||
|
"id": "trigger2",
|
||||||
|
"platform": "conversation",
|
||||||
|
"command": ["test sentence"],
|
||||||
|
},
|
||||||
|
"action": {"set_conversation_response": "response 1"},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
service_response = await hass.services.async_call(
|
||||||
|
"conversation",
|
||||||
|
"process",
|
||||||
|
{"text": "test sentence"},
|
||||||
|
blocking=True,
|
||||||
|
return_response=True,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Should only get first response
|
||||||
|
assert service_response["response"]["speech"]["plain"]["speech"] == "response 1"
|
||||||
|
|
||||||
|
# Service should still have been called
|
||||||
|
assert len(calls) == 1
|
||||||
|
assert calls[0].data["data"] == {
|
||||||
|
"alias": None,
|
||||||
|
"id": "trigger1",
|
||||||
|
"idx": "0",
|
||||||
|
"platform": "conversation",
|
||||||
|
"sentence": "test sentence",
|
||||||
|
"slots": {},
|
||||||
|
"details": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_response_same_sentence_with_error(
|
||||||
|
hass: HomeAssistant, calls, setup_comp, caplog: pytest.LogCaptureFixture
|
||||||
|
) -> None:
|
||||||
|
"""Test the conversation response action with multiple triggers using the same sentence and an error."""
|
||||||
|
caplog.set_level(logging.ERROR)
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
"automation",
|
||||||
|
{
|
||||||
|
"automation": [
|
||||||
|
{
|
||||||
|
"trigger": {
|
||||||
|
"id": "trigger1",
|
||||||
|
"platform": "conversation",
|
||||||
|
"command": ["test sentence"],
|
||||||
|
},
|
||||||
|
"action": [
|
||||||
|
# Add delay so this will not finish first
|
||||||
|
{"delay": "0:0:0.100"},
|
||||||
|
{"service": "fake_domain.fake_service"},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"trigger": {
|
||||||
|
"id": "trigger2",
|
||||||
|
"platform": "conversation",
|
||||||
|
"command": ["test sentence"],
|
||||||
|
},
|
||||||
|
"action": {"set_conversation_response": "response 1"},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
service_response = await hass.services.async_call(
|
||||||
|
"conversation",
|
||||||
|
"process",
|
||||||
|
{"text": "test sentence"},
|
||||||
|
blocking=True,
|
||||||
|
return_response=True,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Should still get first response
|
||||||
|
assert service_response["response"]["speech"]["plain"]["speech"] == "response 1"
|
||||||
|
|
||||||
|
# Error should have been logged
|
||||||
|
assert "Error executing script" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
async def test_subscribe_trigger_does_not_interfere_with_responses(
|
async def test_subscribe_trigger_does_not_interfere_with_responses(
|
||||||
hass: HomeAssistant, setup_comp, hass_ws_client: WebSocketGenerator
|
hass: HomeAssistant, setup_comp, hass_ws_client: WebSocketGenerator
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
"""Test the Emulated Hue component."""
|
"""Test the Emulated Hue component."""
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import AsyncMock, patch
|
from unittest.mock import AsyncMock, Mock, patch
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
|
|
||||||
from homeassistant.components.emulated_hue.config import (
|
from homeassistant.components.emulated_hue.config import (
|
||||||
DATA_KEY,
|
DATA_KEY,
|
||||||
@ -135,6 +137,9 @@ async def test_setup_works(hass: HomeAssistant) -> None:
|
|||||||
AsyncMock(),
|
AsyncMock(),
|
||||||
) as mock_create_upnp_datagram_endpoint, patch(
|
) as mock_create_upnp_datagram_endpoint, patch(
|
||||||
"homeassistant.components.emulated_hue.async_get_source_ip"
|
"homeassistant.components.emulated_hue.async_get_source_ip"
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.emulated_hue.web.TCPSite",
|
||||||
|
return_value=Mock(spec_set=web.TCPSite),
|
||||||
):
|
):
|
||||||
mock_create_upnp_datagram_endpoint.return_value = AsyncMock(
|
mock_create_upnp_datagram_endpoint.return_value = AsyncMock(
|
||||||
spec=UPNPResponderProtocol
|
spec=UPNPResponderProtocol
|
||||||
|
@ -293,7 +293,7 @@ async def test_setup_api_push_api_data(
|
|||||||
assert aioclient_mock.call_count == 19
|
assert aioclient_mock.call_count == 19
|
||||||
assert not aioclient_mock.mock_calls[1][2]["ssl"]
|
assert not aioclient_mock.mock_calls[1][2]["ssl"]
|
||||||
assert aioclient_mock.mock_calls[1][2]["port"] == 9999
|
assert aioclient_mock.mock_calls[1][2]["port"] == 9999
|
||||||
assert aioclient_mock.mock_calls[1][2]["watchdog"]
|
assert "watchdog" not in aioclient_mock.mock_calls[1][2]
|
||||||
|
|
||||||
|
|
||||||
async def test_setup_api_push_api_data_server_host(
|
async def test_setup_api_push_api_data_server_host(
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
"""Tests for Intent component."""
|
"""Tests for Intent component."""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.cover import SERVICE_OPEN_COVER
|
from homeassistant.components.cover import SERVICE_OPEN_COVER
|
||||||
@ -225,6 +226,30 @@ async def test_turn_on_multiple_intent(hass: HomeAssistant) -> None:
|
|||||||
assert call.data == {"entity_id": ["light.test_lights_2"]}
|
assert call.data == {"entity_id": ["light.test_lights_2"]}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_turn_on_all(hass: HomeAssistant) -> None:
|
||||||
|
"""Test HassTurnOn intent with "all" name."""
|
||||||
|
result = await async_setup_component(hass, "homeassistant", {})
|
||||||
|
result = await async_setup_component(hass, "intent", {})
|
||||||
|
assert result
|
||||||
|
|
||||||
|
hass.states.async_set("light.test_light", "off")
|
||||||
|
hass.states.async_set("light.test_light_2", "off")
|
||||||
|
calls = async_mock_service(hass, "light", SERVICE_TURN_ON)
|
||||||
|
|
||||||
|
await intent.async_handle(hass, "test", "HassTurnOn", {"name": {"value": "all"}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# All lights should be on now
|
||||||
|
assert len(calls) == 2
|
||||||
|
entity_ids = set()
|
||||||
|
for call in calls:
|
||||||
|
assert call.domain == "light"
|
||||||
|
assert call.service == "turn_on"
|
||||||
|
entity_ids.update(call.data.get("entity_id", []))
|
||||||
|
|
||||||
|
assert entity_ids == {"light.test_light", "light.test_light_2"}
|
||||||
|
|
||||||
|
|
||||||
async def test_get_state_intent(
|
async def test_get_state_intent(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
area_registry: ar.AreaRegistry,
|
area_registry: ar.AreaRegistry,
|
||||||
|
@ -144,10 +144,10 @@ async def test_node_added_subscription(
|
|||||||
integration: MagicMock,
|
integration: MagicMock,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test subscription to new devices work."""
|
"""Test subscription to new devices work."""
|
||||||
assert matter_client.subscribe_events.call_count == 4
|
assert matter_client.subscribe_events.call_count == 5
|
||||||
assert (
|
assert (
|
||||||
matter_client.subscribe_events.call_args.kwargs["event_filter"]
|
matter_client.subscribe_events.call_args.kwargs["event_filter"]
|
||||||
== EventType.NODE_ADDED
|
== EventType.NODE_UPDATED
|
||||||
)
|
)
|
||||||
|
|
||||||
node_added_callback = matter_client.subscribe_events.call_args.kwargs["callback"]
|
node_added_callback = matter_client.subscribe_events.call_args.kwargs["callback"]
|
||||||
|
@ -229,6 +229,7 @@ async def test_node_diagnostics(
|
|||||||
mac_address="00:11:22:33:44:55",
|
mac_address="00:11:22:33:44:55",
|
||||||
available=True,
|
available=True,
|
||||||
active_fabrics=[MatterFabricData(2, 4939, 1, vendor_name="Nabu Casa")],
|
active_fabrics=[MatterFabricData(2, 4939, 1, vendor_name="Nabu Casa")],
|
||||||
|
active_fabric_index=0,
|
||||||
)
|
)
|
||||||
matter_client.node_diagnostics = AsyncMock(return_value=mock_diagnostics)
|
matter_client.node_diagnostics = AsyncMock(return_value=mock_diagnostics)
|
||||||
|
|
||||||
|
@ -42,6 +42,8 @@ from homeassistant.components.modbus.const import (
|
|||||||
CONF_HVAC_MODE_REGISTER,
|
CONF_HVAC_MODE_REGISTER,
|
||||||
CONF_HVAC_MODE_VALUES,
|
CONF_HVAC_MODE_VALUES,
|
||||||
CONF_HVAC_ONOFF_REGISTER,
|
CONF_HVAC_ONOFF_REGISTER,
|
||||||
|
CONF_MAX_TEMP,
|
||||||
|
CONF_MIN_TEMP,
|
||||||
CONF_TARGET_TEMP,
|
CONF_TARGET_TEMP,
|
||||||
CONF_TARGET_TEMP_WRITE_REGISTERS,
|
CONF_TARGET_TEMP_WRITE_REGISTERS,
|
||||||
CONF_WRITE_REGISTERS,
|
CONF_WRITE_REGISTERS,
|
||||||
@ -170,6 +172,30 @@ ENTITY_ID = f"{CLIMATE_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_")
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
CONF_CLIMATES: [
|
||||||
|
{
|
||||||
|
CONF_NAME: TEST_ENTITY_NAME,
|
||||||
|
CONF_TARGET_TEMP: 117,
|
||||||
|
CONF_ADDRESS: 117,
|
||||||
|
CONF_SLAVE: 10,
|
||||||
|
CONF_MIN_TEMP: 23,
|
||||||
|
CONF_MAX_TEMP: 57,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CONF_CLIMATES: [
|
||||||
|
{
|
||||||
|
CONF_NAME: TEST_ENTITY_NAME,
|
||||||
|
CONF_TARGET_TEMP: 117,
|
||||||
|
CONF_ADDRESS: 117,
|
||||||
|
CONF_SLAVE: 10,
|
||||||
|
CONF_MIN_TEMP: -57,
|
||||||
|
CONF_MAX_TEMP: -23,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_config_climate(hass: HomeAssistant, mock_modbus) -> None:
|
async def test_config_climate(hass: HomeAssistant, mock_modbus) -> None:
|
||||||
|
@ -185,6 +185,28 @@ SLAVE_UNIQUE_ID = "ground_floor_sensor"
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
CONF_SENSORS: [
|
||||||
|
{
|
||||||
|
CONF_NAME: TEST_ENTITY_NAME,
|
||||||
|
CONF_ADDRESS: 51,
|
||||||
|
CONF_DATA_TYPE: DataType.INT16,
|
||||||
|
CONF_MIN_VALUE: 1,
|
||||||
|
CONF_MAX_VALUE: 3,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
CONF_SENSORS: [
|
||||||
|
{
|
||||||
|
CONF_NAME: TEST_ENTITY_NAME,
|
||||||
|
CONF_ADDRESS: 51,
|
||||||
|
CONF_DATA_TYPE: DataType.INT16,
|
||||||
|
CONF_MIN_VALUE: -3,
|
||||||
|
CONF_MAX_VALUE: -1,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_config_sensor(hass: HomeAssistant, mock_modbus) -> None:
|
async def test_config_sensor(hass: HomeAssistant, mock_modbus) -> None:
|
||||||
@ -688,6 +710,16 @@ async def test_config_wrong_struct_sensor(
|
|||||||
False,
|
False,
|
||||||
"112594",
|
"112594",
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
CONF_DATA_TYPE: DataType.INT16,
|
||||||
|
CONF_SCALE: -1,
|
||||||
|
CONF_OFFSET: 0,
|
||||||
|
},
|
||||||
|
[0x000A],
|
||||||
|
False,
|
||||||
|
"-10",
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_all_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> None:
|
async def test_all_sensor(hass: HomeAssistant, mock_do_cycle, expected) -> None:
|
||||||
|
@ -4257,3 +4257,64 @@ async def test_update_entry_and_reload(
|
|||||||
assert entry.state == config_entries.ConfigEntryState.LOADED
|
assert entry.state == config_entries.ConfigEntryState.LOADED
|
||||||
assert task["type"] == FlowResultType.ABORT
|
assert task["type"] == FlowResultType.ABORT
|
||||||
assert task["reason"] == "reauth_successful"
|
assert task["reason"] == "reauth_successful"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("unique_id", [["blah", "bleh"], {"key": "value"}])
|
||||||
|
async def test_unhashable_unique_id(
|
||||||
|
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, unique_id: Any
|
||||||
|
) -> None:
|
||||||
|
"""Test the ConfigEntryItems user dict handles unhashable unique_id."""
|
||||||
|
entries = config_entries.ConfigEntryItems(hass)
|
||||||
|
entry = config_entries.ConfigEntry(
|
||||||
|
version=1,
|
||||||
|
minor_version=1,
|
||||||
|
domain="test",
|
||||||
|
entry_id="mock_id",
|
||||||
|
title="title",
|
||||||
|
data={},
|
||||||
|
source="test",
|
||||||
|
unique_id=unique_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
entries[entry.entry_id] = entry
|
||||||
|
assert (
|
||||||
|
"Config entry 'title' from integration test has an invalid unique_id "
|
||||||
|
f"'{str(unique_id)}'"
|
||||||
|
) in caplog.text
|
||||||
|
|
||||||
|
assert entry.entry_id in entries
|
||||||
|
assert entries[entry.entry_id] is entry
|
||||||
|
assert entries.get_entry_by_domain_and_unique_id("test", unique_id) == entry
|
||||||
|
del entries[entry.entry_id]
|
||||||
|
assert not entries
|
||||||
|
assert entries.get_entry_by_domain_and_unique_id("test", unique_id) is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("unique_id", [123])
|
||||||
|
async def test_hashable_non_string_unique_id(
|
||||||
|
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, unique_id: Any
|
||||||
|
) -> None:
|
||||||
|
"""Test the ConfigEntryItems user dict handles hashable non string unique_id."""
|
||||||
|
entries = config_entries.ConfigEntryItems(hass)
|
||||||
|
entry = config_entries.ConfigEntry(
|
||||||
|
version=1,
|
||||||
|
minor_version=1,
|
||||||
|
domain="test",
|
||||||
|
entry_id="mock_id",
|
||||||
|
title="title",
|
||||||
|
data={},
|
||||||
|
source="test",
|
||||||
|
unique_id=unique_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
entries[entry.entry_id] = entry
|
||||||
|
assert (
|
||||||
|
"Config entry 'title' from integration test has an invalid unique_id"
|
||||||
|
) not in caplog.text
|
||||||
|
|
||||||
|
assert entry.entry_id in entries
|
||||||
|
assert entries[entry.entry_id] is entry
|
||||||
|
assert entries.get_entry_by_domain_and_unique_id("test", unique_id) == entry
|
||||||
|
del entries[entry.entry_id]
|
||||||
|
assert not entries
|
||||||
|
assert entries.get_entry_by_domain_and_unique_id("test", unique_id) is None
|
||||||
|
Loading…
x
Reference in New Issue
Block a user