Skip to content

Commit

Permalink
2024.2.1 (#110078)
Browse files Browse the repository at this point in the history
  • Loading branch information
frenck authored Feb 9, 2024
2 parents 9dbf842 + 5f9cc2f commit cfd1f78
Show file tree
Hide file tree
Showing 38 changed files with 873 additions and 108 deletions.
2 changes: 1 addition & 1 deletion homeassistant/components/aosmith/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/aosmith",
"iot_class": "cloud_polling",
"requirements": ["py-aosmith==1.0.6"]
"requirements": ["py-aosmith==1.0.8"]
}
34 changes: 25 additions & 9 deletions homeassistant/components/climate/intent.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Intents for the client integration."""

from __future__ import annotations

import voluptuous as vol
Expand Down Expand Up @@ -36,32 +37,47 @@ async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse
if not entities:
raise intent.IntentHandleError("No climate entities")

if "area" in slots:
# Filter by area
area_name = slots["area"]["value"]
name_slot = slots.get("name", {})
entity_name: str | None = name_slot.get("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(
hass, area_name=area_name, domains=[DOMAIN]
hass, name=entity_name, area_name=area_id, domains=[DOMAIN]
):
climate_state = maybe_climate
break

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)
elif "name" in slots:
elif entity_name:
# Filter by name
entity_name = slots["name"]["value"]

for maybe_climate in intent.async_match_states(
hass, name=entity_name, domains=[DOMAIN]
):
climate_state = maybe_climate
break

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)
else:
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/co2signal/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["aioelectricitymaps"],
"requirements": ["aioelectricitymaps==0.3.1"]
"requirements": ["aioelectricitymaps==0.4.0"]
}
64 changes: 50 additions & 14 deletions homeassistant/components/conversation/default_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,22 +223,22 @@ async def async_process(self, user_input: ConversationInput) -> ConversationResu
# Check if a trigger matched
if isinstance(result, SentenceTriggerResult):
# Gather callback responses in parallel
trigger_responses = await asyncio.gather(
*(
self._trigger_sentences[trigger_id].callback(
result.sentence, trigger_result
)
for trigger_id, trigger_result in result.matched_triggers.items()
trigger_callbacks = [
self._trigger_sentences[trigger_id].callback(
result.sentence, trigger_result
)
)
for trigger_id, trigger_result in result.matched_triggers.items()
]

# Use last non-empty result as response.
#
# 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.
response_text: str | None = None
for trigger_response in trigger_responses:
response_text = response_text or trigger_response
for trigger_future in asyncio.as_completed(trigger_callbacks):
if trigger_response := await trigger_future:
response_text = trigger_response
break

# Convert to conversation result
response = intent.IntentResponse(language=language)
Expand Down Expand Up @@ -316,6 +316,20 @@ async def async_process(self, user_input: ConversationInput) -> ConversationResu
),
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:
# Intent was valid and entities matched constraints, but an error
# occurred during handling.
Expand Down Expand Up @@ -724,7 +738,12 @@ def _make_slot_lists(self) -> dict[str, SlotList]:
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 = []
for state in states:
# Checked against "requires_context" and "excludes_context" in hassil
Expand All @@ -740,20 +759,23 @@ def _make_slot_lists(self) -> dict[str, SlotList]:

if not entity:
# Default name
entity_names.append((state.name, state.entity_id, context))
entity_names.append((state.name, state.name, context))
continue

if entity.aliases:
for alias in entity.aliases:
if not alias.strip():
continue

entity_names.append((alias, state.entity_id, context))
entity_names.append((alias, alias, context))

# 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)
area_names = []
for area in areas.async_list_areas():
Expand Down Expand Up @@ -984,6 +1006,20 @@ def _get_no_states_matched_response(
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:
"""Collect list reference names recursively."""
if isinstance(expression, Sequence):
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/ecovacs/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
"iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
"requirements": ["py-sucks==0.9.8", "deebot-client==5.1.0"]
"requirements": ["py-sucks==0.9.8", "deebot-client==5.1.1"]
}
2 changes: 1 addition & 1 deletion homeassistant/components/ecowitt/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
"dependencies": ["webhook"],
"documentation": "https://www.home-assistant.io/integrations/ecowitt",
"iot_class": "local_push",
"requirements": ["aioecowitt==2024.2.0"]
"requirements": ["aioecowitt==2024.2.1"]
}
2 changes: 1 addition & 1 deletion homeassistant/components/evohome/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/evohome",
"iot_class": "cloud_polling",
"loggers": ["evohomeasync", "evohomeasync2"],
"requirements": ["evohome-async==0.4.17"]
"requirements": ["evohome-async==0.4.18"]
}
2 changes: 1 addition & 1 deletion homeassistant/components/frontend/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20240207.0"]
"requirements": ["home-assistant-frontend==20240207.1"]
}
2 changes: 1 addition & 1 deletion homeassistant/components/geonetnz_volcano/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["aio_geojson_geonetnz_volcano"],
"requirements": ["aio-geojson-geonetnz-volcano==0.8"]
"requirements": ["aio-geojson-geonetnz-volcano==0.9"]
}
1 change: 0 additions & 1 deletion homeassistant/components/hassio/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,6 @@ async def update_hass_api(
options = {
"ssl": CONF_SSL_CERTIFICATE in http_config,
"port": port,
"watchdog": True,
"refresh_token": refresh_token.token,
}

Expand Down
13 changes: 10 additions & 3 deletions homeassistant/components/honeywell/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from aiohttp import ClientConnectionError
from aiosomecomfort import (
APIRateLimited,
AuthError,
ConnectionError as AscConnectionError,
SomeComfortError,
Expand Down Expand Up @@ -505,10 +506,11 @@ async def _login() -> None:
await self._device.refresh()

except (
asyncio.TimeoutError,
AscConnectionError,
APIRateLimited,
AuthError,
ClientConnectionError,
AscConnectionError,
asyncio.TimeoutError,
):
self._retry += 1
self._attr_available = self._retry <= RETRY
Expand All @@ -524,7 +526,12 @@ async def _login() -> None:
await _login()
return

except (AscConnectionError, ClientConnectionError, asyncio.TimeoutError):
except (
asyncio.TimeoutError,
AscConnectionError,
APIRateLimited,
ClientConnectionError,
):
self._retry += 1
self._attr_available = self._retry <= RETRY
return
Expand Down
26 changes: 18 additions & 8 deletions homeassistant/components/intent/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""The Intent integration."""

from __future__ import annotations

import logging
Expand Down Expand Up @@ -155,16 +156,18 @@ async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse
slots = self.async_validate_slots(intent_obj.slots)

# 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
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
if area_name is not None:
if area_id is not None:
areas = ar.async_get(hass)
area = areas.async_get_area(area_name) or areas.async_get_area_by_name(
area_name
)
area = areas.async_get_area(area_id)
if area is None:
raise intent.IntentHandleError(f"No area named {area_name}")

Expand All @@ -186,7 +189,7 @@ async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse
states = list(
intent.async_match_states(
hass,
name=name,
name=entity_name,
area=area,
domains=domains,
device_classes=device_classes,
Expand All @@ -197,13 +200,20 @@ async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse
_LOGGER.debug(
"Found %s state(s) that matched: name=%s, area=%s, domains=%s, device_classes=%s, assistant=%s",
len(states),
name,
entity_name,
area,
domains,
device_classes,
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
response = intent_obj.create_response()
response.response_type = intent.IntentResponseType.QUERY_ANSWER
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/keymitt_ble/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@
"integration_type": "hub",
"iot_class": "assumed_state",
"loggers": ["keymitt_ble"],
"requirements": ["PyMicroBot==0.0.10"]
"requirements": ["PyMicroBot==0.0.12"]
}
21 changes: 21 additions & 0 deletions homeassistant/components/matter/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,27 @@ def register_platform_handler(

async def setup_nodes(self) -> None:
"""Set up all existing nodes and subscribe to new nodes."""
initialized_nodes: set[int] = set()
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)

def node_added_callback(event: EventType, node: MatterNode) -> None:
"""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)

def endpoint_added_callback(event: EventType, data: dict[str, int]) -> None:
Expand Down Expand Up @@ -116,6 +132,11 @@ def node_removed_callback(event: EventType, node_id: int) -> None:
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:
"""Set up an node."""
Expand Down
3 changes: 3 additions & 0 deletions homeassistant/components/matter/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ async def async_will_remove_from_hass(self) -> None:

async def async_update(self) -> None:
"""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
await self.matter_client.refresh_attribute(
self._endpoint.node.node_id,
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/matter/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
"dependencies": ["websocket_api"],
"documentation": "https://www.home-assistant.io/integrations/matter",
"iot_class": "local_push",
"requirements": ["python-matter-server==5.4.1"]
"requirements": ["python-matter-server==5.5.0"]
}
Loading

0 comments on commit cfd1f78

Please sign in to comment.