Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 43 additions & 23 deletions homeassistant/components/conversation/default_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,8 @@ class IntentCacheKey:
language: str
"""Language of text."""

device_id: str | None
"""Device id from user input."""
satellite_id: str | None
"""Satellite id from user input."""


@dataclass(frozen=True)
Expand Down Expand Up @@ -443,9 +443,15 @@ async def _async_process_intent_result(
}
for entity in result.entities_list
}
device_area = self._get_device_area(user_input.device_id)
if device_area:
slots["preferred_area_id"] = {"value": device_area.id}

satellite_id = user_input.satellite_id
device_id = user_input.device_id
satellite_area, device_id = self._get_satellite_area_and_device(
satellite_id, device_id
)
if satellite_area is not None:
slots["preferred_area_id"] = {"value": satellite_area.id}

async_conversation_trace_append(
ConversationTraceEventType.TOOL_CALL,
{
Expand All @@ -467,8 +473,8 @@ async def _async_process_intent_result(
user_input.context,
language,
assistant=DOMAIN,
device_id=user_input.device_id,
satellite_id=user_input.satellite_id,
device_id=device_id,
satellite_id=satellite_id,
conversation_agent_id=user_input.agent_id,
)
except intent.MatchFailedError as match_error:
Expand Down Expand Up @@ -534,7 +540,9 @@ def _recognize(

# Try cache first
cache_key = IntentCacheKey(
text=user_input.text, language=language, device_id=user_input.device_id
text=user_input.text,
language=language,
satellite_id=user_input.satellite_id,
)
cache_value = self._intent_cache.get(cache_key)
if cache_value is not None:
Expand Down Expand Up @@ -1304,28 +1312,40 @@ def _make_intent_context(
self, user_input: ConversationInput
) -> dict[str, Any] | None:
"""Return intent recognition context for user input."""
if not user_input.device_id:
satellite_area, _ = self._get_satellite_area_and_device(
user_input.satellite_id, user_input.device_id
)
if satellite_area is None:
return None

device_area = self._get_device_area(user_input.device_id)
if device_area is None:
return None
return {"area": {"value": satellite_area.name, "text": satellite_area.name}}

return {"area": {"value": device_area.name, "text": device_area.name}}
def _get_satellite_area_and_device(
self, satellite_id: str | None, device_id: str | None = None
) -> tuple[ar.AreaEntry | None, str | None]:
"""Return area entry and device id."""
hass = self.hass

def _get_device_area(self, device_id: str | None) -> ar.AreaEntry | None:
"""Return area object for given device identifier."""
if device_id is None:
return None
area_id: str | None = None

devices = dr.async_get(self.hass)
device = devices.async_get(device_id)
if (device is None) or (device.area_id is None):
return None
if (
satellite_id is not None
and (entity_entry := er.async_get(hass).async_get(satellite_id)) is not None
):
area_id = entity_entry.area_id
device_id = entity_entry.device_id

areas = ar.async_get(self.hass)
if (
area_id is None
and device_id is not None
and (device_entry := dr.async_get(hass).async_get(device_id)) is not None
):
area_id = device_entry.area_id

if area_id is None:
return None, device_id

return areas.async_get_area(device.area_id)
return ar.async_get(hass).async_get_area(area_id), device_id

def _get_error_text(
self,
Expand Down
16 changes: 13 additions & 3 deletions homeassistant/components/conversation/trigger.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

from homeassistant.const import CONF_COMMAND, CONF_PLATFORM
from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import config_validation as cv, entity_registry as er
from homeassistant.helpers.script import ScriptRunResult
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
from homeassistant.helpers.typing import UNDEFINED, ConfigType
Expand Down Expand Up @@ -71,6 +71,8 @@ async def async_attach_trigger(
trigger_data = trigger_info["trigger_data"]
sentences = config.get(CONF_COMMAND, [])

ent_reg = er.async_get(hass)

job = HassJob(action)

async def call_action(
Expand All @@ -92,6 +94,14 @@ async def call_action(
for entity_name, entity in result.entities.items()
}

satellite_id = user_input.satellite_id
device_id = user_input.device_id
if (
satellite_id is not None
and (satellite_entry := ent_reg.async_get(satellite_id)) is not None
):
device_id = satellite_entry.device_id

trigger_input: dict[str, Any] = { # Satisfy type checker
**trigger_data,
"platform": DOMAIN,
Expand All @@ -100,8 +110,8 @@ async def call_action(
"slots": { # direct access to values
entity_name: entity["value"] for entity_name, entity in details.items()
},
"device_id": user_input.device_id,
"satellite_id": user_input.satellite_id,
"device_id": device_id,
"satellite_id": satellite_id,
"user_input": user_input.as_dict(),
}

Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/esphome/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ESPHomeConfigEntry) -> b
client_info=CLIENT_INFO,
zeroconf_instance=zeroconf_instance,
noise_psk=noise_psk,
timezone=hass.config.time_zone,
)

domain_data = DomainData.get(hass)
Expand Down
46 changes: 8 additions & 38 deletions homeassistant/components/ezviz/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,26 +66,6 @@
key="last_alarm_type_name",
translation_key="last_alarm_type_name",
),
"Record_Mode": SensorEntityDescription(
key="Record_Mode",
translation_key="record_mode",
entity_registry_enabled_default=False,
),
"battery_camera_work_mode": SensorEntityDescription(
key="battery_camera_work_mode",
translation_key="battery_camera_work_mode",
entity_registry_enabled_default=False,
),
"powerStatus": SensorEntityDescription(
key="powerStatus",
translation_key="power_status",
entity_registry_enabled_default=False,
),
"OnlineStatus": SensorEntityDescription(
key="OnlineStatus",
translation_key="online_status",
entity_registry_enabled_default=False,
),
}


Expand All @@ -96,26 +76,16 @@ async def async_setup_entry(
) -> None:
"""Set up EZVIZ sensors based on a config entry."""
coordinator = entry.runtime_data
entities: list[EzvizSensor] = []

for camera, sensors in coordinator.data.items():
entities.extend(
async_add_entities(
[
EzvizSensor(coordinator, camera, sensor)
for sensor, value in sensors.items()
if sensor in SENSOR_TYPES and value is not None
)

optionals = sensors.get("optionals", {})
entities.extend(
EzvizSensor(coordinator, camera, optional_key)
for optional_key in ("powerStatus", "OnlineStatus")
if optional_key in optionals
)

if "mode" in optionals.get("Record_Mode", {}):
entities.append(EzvizSensor(coordinator, camera, "mode"))

async_add_entities(entities)
for camera in coordinator.data
for sensor, value in coordinator.data[camera].items()
if sensor in SENSOR_TYPES
if value is not None
]
)


class EzvizSensor(EzvizEntity, SensorEntity):
Expand Down
12 changes: 0 additions & 12 deletions homeassistant/components/ezviz/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -147,18 +147,6 @@
},
"last_alarm_type_name": {
"name": "Last alarm type name"
},
"record_mode": {
"name": "Record mode"
},
"battery_camera_work_mode": {
"name": "Battery work mode"
},
"power_status": {
"name": "Power status"
},
"online_status": {
"name": "Online status"
}
},
"switch": {
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/iron_os/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@
"iot_class": "local_polling",
"loggers": ["pynecil"],
"quality_scale": "platinum",
"requirements": ["pynecil==4.1.1"]
"requirements": ["pynecil==4.2.0"]
}
2 changes: 1 addition & 1 deletion homeassistant/components/mcp/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/mcp",
"iot_class": "local_polling",
"quality_scale": "silver",
"requirements": ["mcp==1.5.0"]
"requirements": ["mcp==1.14.1"]
}
19 changes: 11 additions & 8 deletions homeassistant/components/mcp_server/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import anyio
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
from mcp import types
from mcp.shared.message import SessionMessage

from homeassistant.components import conversation
from homeassistant.components.http import KEY_HASS, HomeAssistantView
Expand Down Expand Up @@ -98,12 +99,12 @@ async def get(self, request: web.Request) -> web.StreamResponse:
server.create_initialization_options # Reads package for version info
)

read_stream: MemoryObjectReceiveStream[types.JSONRPCMessage | Exception]
read_stream_writer: MemoryObjectSendStream[types.JSONRPCMessage | Exception]
read_stream: MemoryObjectReceiveStream[SessionMessage | Exception]
read_stream_writer: MemoryObjectSendStream[SessionMessage | Exception]
read_stream_writer, read_stream = anyio.create_memory_object_stream(0)

write_stream: MemoryObjectSendStream[types.JSONRPCMessage]
write_stream_reader: MemoryObjectReceiveStream[types.JSONRPCMessage]
write_stream: MemoryObjectSendStream[SessionMessage]
write_stream_reader: MemoryObjectReceiveStream[SessionMessage]
write_stream, write_stream_reader = anyio.create_memory_object_stream(0)

async with (
Expand All @@ -116,10 +117,12 @@ async def get(self, request: web.Request) -> web.StreamResponse:

async def sse_reader() -> None:
"""Forward MCP server responses to the client."""
async for message in write_stream_reader:
_LOGGER.debug("Sending SSE message: %s", message)
async for session_message in write_stream_reader:
_LOGGER.debug("Sending SSE message: %s", session_message)
await response.send(
message.model_dump_json(by_alias=True, exclude_none=True),
session_message.message.model_dump_json(
by_alias=True, exclude_none=True
),
event="message",
)

Expand Down Expand Up @@ -163,5 +166,5 @@ async def post(
raise HTTPBadRequest(text="Could not parse message") from err

_LOGGER.debug("Received client message: %s", message)
await session.read_stream_writer.send(message)
await session.read_stream_writer.send(SessionMessage(message))
return web.Response(status=200)
2 changes: 1 addition & 1 deletion homeassistant/components/mcp_server/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
"integration_type": "service",
"iot_class": "local_push",
"quality_scale": "silver",
"requirements": ["mcp==1.5.0", "aiohttp_sse==2.2.0", "anyio==4.10.0"],
"requirements": ["mcp==1.14.1", "aiohttp_sse==2.2.0", "anyio==4.10.0"],
"single_config_entry": true
}
2 changes: 1 addition & 1 deletion homeassistant/components/mcp_server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ async def list_tools() -> list[types.Tool]:
llm_api = await get_api_instance()
return [_format_tool(tool, llm_api.custom_serializer) for tool in llm_api.tools]

@server.call_tool() # type: ignore[no-untyped-call, misc]
@server.call_tool() # type: ignore[misc]
async def call_tool(name: str, arguments: dict) -> Sequence[types.TextContent]:
"""Handle calling tools."""
llm_api = await get_api_instance()
Expand Down
4 changes: 2 additions & 2 deletions homeassistant/components/mcp_server/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import logging

from anyio.streams.memory import MemoryObjectSendStream
from mcp import types
from mcp.shared.message import SessionMessage

from homeassistant.util import ulid as ulid_util

Expand All @@ -22,7 +22,7 @@
class Session:
"""A session for the Model Context Protocol."""

read_stream_writer: MemoryObjectSendStream[types.JSONRPCMessage | Exception]
read_stream_writer: MemoryObjectSendStream[SessionMessage | Exception]


class SessionManager:
Expand Down
36 changes: 29 additions & 7 deletions homeassistant/components/music_assistant/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ async def async_step_user(
self.server_info.server_id, raise_on_progress=False
)
self._abort_if_unique_id_configured(
updates={CONF_URL: self.server_info.base_url},
updates={CONF_URL: user_input[CONF_URL]},
reload_on_update=True,
)
except CannotConnect:
Expand All @@ -82,7 +82,7 @@ async def async_step_user(
return self.async_create_entry(
title=DEFAULT_TITLE,
data={
CONF_URL: self.server_info.base_url,
CONF_URL: user_input[CONF_URL],
},
)

Expand All @@ -103,14 +103,36 @@ async def async_step_zeroconf(
# abort if discovery info is not what we expect
if "server_id" not in discovery_info.properties:
return self.async_abort(reason="missing_server_id")
# abort if we already have exactly this server_id
# reload the integration if the host got updated

self.server_info = ServerInfoMessage.from_dict(discovery_info.properties)
await self.async_set_unique_id(self.server_info.server_id)
self._abort_if_unique_id_configured(
updates={CONF_URL: self.server_info.base_url},
reload_on_update=True,

# Check if we already have a config entry for this server_id
existing_entry = self.hass.config_entries.async_entry_for_domain_unique_id(
DOMAIN, self.server_info.server_id
)

if existing_entry:
# Test connectivity to the current URL first
current_url = existing_entry.data[CONF_URL]
try:
await get_server_info(self.hass, current_url)
# Current URL is working, no need to update
return self.async_abort(reason="already_configured")
except CannotConnect:
# Current URL is not working, update to the discovered URL
# and continue to discovery confirm
self.hass.config_entries.async_update_entry(
existing_entry,
data={**existing_entry.data, CONF_URL: self.server_info.base_url},
)
# Schedule reload since URL changed
self.hass.config_entries.async_schedule_reload(existing_entry.entry_id)
else:
# No existing entry, proceed with normal flow
self._abort_if_unique_id_configured()

# Test connectivity to the discovered URL
try:
await get_server_info(self.hass, self.server_info.base_url)
except CannotConnect:
Expand Down
Loading
Loading