[FIX] Update script to use SupportsResponse and return ServiceResponse
This commit is contained in:
parent
09a500904c
commit
15f5918e20
@ -3,13 +3,20 @@
|
|||||||
This integration provides services to expose filtered entities and scripts
|
This integration provides services to expose filtered entities and scripts
|
||||||
to MCP (Model Context Protocol) servers for AI agent control.
|
to MCP (Model Context Protocol) servers for AI agent control.
|
||||||
"""
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant, ServiceCall
|
from homeassistant.const import CONF_ENTITY_ID
|
||||||
|
from homeassistant.core import (
|
||||||
|
HomeAssistant,
|
||||||
|
ServiceCall,
|
||||||
|
ServiceResponse,
|
||||||
|
SupportsResponse,
|
||||||
|
)
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
from homeassistant.const import CONF_ENTITY_ID
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -23,118 +30,141 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
"""Set up the MCP Bridge integration."""
|
"""Set up the MCP Bridge integration."""
|
||||||
_LOGGER.info("Setting up MCP Bridge integration")
|
_LOGGER.info("Setting up MCP Bridge integration")
|
||||||
|
|
||||||
async def get_exposed_entities(call: ServiceCall) -> dict[str, Any]:
|
async def get_exposed_entities(call: ServiceCall) -> ServiceResponse:
|
||||||
"""Get all entities exposed to voice assistants."""
|
"""Get all entities exposed to conversation / voice assistants."""
|
||||||
entity_reg = er.async_get(hass)
|
entity_reg = er.async_get(hass)
|
||||||
|
area_reg = hass.helpers.area_registry.async_get(hass)
|
||||||
|
|
||||||
exposed_entities = []
|
exposed_entities: list[dict[str, Any]] = []
|
||||||
|
|
||||||
# Iterate through all entities
|
|
||||||
for state in hass.states.async_all():
|
for state in hass.states.async_all():
|
||||||
entity_id = state.entity_id
|
entity_id = state.entity_id
|
||||||
entity_entry = entity_reg.async_get(entity_id)
|
entity_entry = entity_reg.async_get(entity_id)
|
||||||
|
|
||||||
# Check if entity is exposed to conversation (voice assistant)
|
if not entity_entry:
|
||||||
if entity_entry and entity_entry.options.get("conversation", {}).get("should_expose", False):
|
continue
|
||||||
# Get area name if available
|
|
||||||
area_name = None
|
|
||||||
if entity_entry.area_id:
|
|
||||||
area_reg = hass.helpers.area_registry.async_get(hass)
|
|
||||||
area = area_reg.async_get_area(entity_entry.area_id)
|
|
||||||
if area:
|
|
||||||
area_name = area.name
|
|
||||||
|
|
||||||
# Build entity data
|
if not entity_entry.options.get("conversation", {}).get(
|
||||||
entity_data = {
|
"should_expose", False
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
|
||||||
|
area_name = None
|
||||||
|
if entity_entry.area_id:
|
||||||
|
area = area_reg.async_get_area(entity_entry.area_id)
|
||||||
|
if area:
|
||||||
|
area_name = area.name
|
||||||
|
|
||||||
|
exposed_entities.append(
|
||||||
|
{
|
||||||
"entity_id": entity_id,
|
"entity_id": entity_id,
|
||||||
"state": state.state,
|
"state": state.state,
|
||||||
"friendly_name": state.attributes.get("friendly_name", entity_id),
|
"friendly_name": state.attributes.get(
|
||||||
|
"friendly_name", entity_id
|
||||||
|
),
|
||||||
"area": area_name,
|
"area": area_name,
|
||||||
"domain": entity_id.split(".")[0],
|
"domain": entity_id.split(".")[0],
|
||||||
"device_class": state.attributes.get("device_class"),
|
"device_class": state.attributes.get("device_class"),
|
||||||
"supported_features": state.attributes.get("supported_features", 0),
|
"supported_features": state.attributes.get(
|
||||||
|
"supported_features", 0
|
||||||
|
),
|
||||||
"last_changed": state.last_changed.isoformat(),
|
"last_changed": state.last_changed.isoformat(),
|
||||||
"last_updated": state.last_updated.isoformat(),
|
"last_updated": state.last_updated.isoformat(),
|
||||||
"attributes": dict(state.attributes)
|
"attributes": dict(state.attributes),
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
exposed_entities.append(entity_data)
|
_LOGGER.debug("Found %s exposed entities", len(exposed_entities))
|
||||||
|
|
||||||
_LOGGER.debug(f"Found {len(exposed_entities)} exposed entities")
|
return ServiceResponse(
|
||||||
|
{
|
||||||
|
"entities": exposed_entities,
|
||||||
|
"count": len(exposed_entities),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
async def get_exposed_scripts(call: ServiceCall) -> ServiceResponse:
|
||||||
"entities": exposed_entities,
|
|
||||||
"count": len(exposed_entities)
|
|
||||||
}
|
|
||||||
|
|
||||||
async def get_exposed_scripts(call: ServiceCall) -> dict[str, Any]:
|
|
||||||
"""Get all scripts marked as MCP-accessible."""
|
"""Get all scripts marked as MCP-accessible."""
|
||||||
entity_reg = er.async_get(hass)
|
entity_reg = er.async_get(hass)
|
||||||
|
|
||||||
exposed_scripts = []
|
exposed_scripts: list[dict[str, Any]] = []
|
||||||
|
|
||||||
# Get all script entities
|
|
||||||
for entity_id in hass.states.async_entity_ids("script"):
|
for entity_id in hass.states.async_entity_ids("script"):
|
||||||
entity_entry = entity_reg.async_get(entity_id)
|
entity_entry = entity_reg.async_get(entity_id)
|
||||||
|
|
||||||
# Check if script has the mcp_accessible label
|
if not entity_entry:
|
||||||
if entity_entry and MCP_ACCESSIBLE_LABEL in entity_entry.labels:
|
continue
|
||||||
state = hass.states.get(entity_id)
|
|
||||||
if not state:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Get script attributes
|
if MCP_ACCESSIBLE_LABEL not in entity_entry.labels:
|
||||||
script_data = {
|
continue
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
if not state:
|
||||||
|
continue
|
||||||
|
|
||||||
|
exposed_scripts.append(
|
||||||
|
{
|
||||||
"entity_id": entity_id,
|
"entity_id": entity_id,
|
||||||
"friendly_name": state.attributes.get("friendly_name", entity_id),
|
"friendly_name": state.attributes.get(
|
||||||
|
"friendly_name", entity_id
|
||||||
|
),
|
||||||
"description": state.attributes.get("description", ""),
|
"description": state.attributes.get("description", ""),
|
||||||
"fields": state.attributes.get("fields", {}),
|
"fields": state.attributes.get("fields", {}),
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
exposed_scripts.append(script_data)
|
_LOGGER.debug("Found %s exposed scripts", len(exposed_scripts))
|
||||||
|
|
||||||
_LOGGER.debug(f"Found {len(exposed_scripts)} exposed scripts")
|
return ServiceResponse(
|
||||||
|
{
|
||||||
|
"scripts": exposed_scripts,
|
||||||
|
"count": len(exposed_scripts),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
async def get_entity_metadata(call: ServiceCall) -> ServiceResponse:
|
||||||
"scripts": exposed_scripts,
|
|
||||||
"count": len(exposed_scripts)
|
|
||||||
}
|
|
||||||
|
|
||||||
async def get_entity_metadata(call: ServiceCall) -> dict[str, Any]:
|
|
||||||
"""Get detailed metadata for a specific entity."""
|
"""Get detailed metadata for a specific entity."""
|
||||||
entity_id = call.data.get(CONF_ENTITY_ID)
|
entity_id = call.data.get(CONF_ENTITY_ID)
|
||||||
|
|
||||||
if not entity_id:
|
if not entity_id:
|
||||||
_LOGGER.error("entity_id not provided")
|
return ServiceResponse(
|
||||||
return {"error": "entity_id required"}
|
{"error": "entity_id is required"},
|
||||||
|
success=False,
|
||||||
|
)
|
||||||
|
|
||||||
entity_reg = er.async_get(hass)
|
entity_reg = er.async_get(hass)
|
||||||
|
area_reg = hass.helpers.area_registry.async_get(hass)
|
||||||
|
|
||||||
entity_entry = entity_reg.async_get(entity_id)
|
entity_entry = entity_reg.async_get(entity_id)
|
||||||
state = hass.states.get(entity_id)
|
state = hass.states.get(entity_id)
|
||||||
|
|
||||||
if not state:
|
if not state:
|
||||||
_LOGGER.error(f"Entity {entity_id} not found")
|
return ServiceResponse(
|
||||||
return {"error": f"Entity {entity_id} not found"}
|
{"error": f"Entity {entity_id} not found"},
|
||||||
|
success=False,
|
||||||
|
)
|
||||||
|
|
||||||
# Get area name if available
|
|
||||||
area_name = None
|
area_name = None
|
||||||
if entity_entry and entity_entry.area_id:
|
if entity_entry and entity_entry.area_id:
|
||||||
area_reg = hass.helpers.area_registry.async_get(hass)
|
|
||||||
area = area_reg.async_get_area(entity_entry.area_id)
|
area = area_reg.async_get_area(entity_entry.area_id)
|
||||||
if area:
|
if area:
|
||||||
area_name = area.name
|
area_name = area.name
|
||||||
|
|
||||||
metadata = {
|
metadata: dict[str, Any] = {
|
||||||
"entity_id": entity_id,
|
"entity_id": entity_id,
|
||||||
"state": state.state,
|
"state": state.state,
|
||||||
"friendly_name": state.attributes.get("friendly_name", entity_id),
|
"friendly_name": state.attributes.get(
|
||||||
|
"friendly_name", entity_id
|
||||||
|
),
|
||||||
"area": area_name,
|
"area": area_name,
|
||||||
"domain": entity_id.split(".")[0],
|
"domain": entity_id.split(".")[0],
|
||||||
"device_class": state.attributes.get("device_class"),
|
"device_class": state.attributes.get("device_class"),
|
||||||
"supported_features": state.attributes.get("supported_features", 0),
|
"supported_features": state.attributes.get(
|
||||||
|
"supported_features", 0
|
||||||
|
),
|
||||||
"last_changed": state.last_changed.isoformat(),
|
"last_changed": state.last_changed.isoformat(),
|
||||||
"last_updated": state.last_updated.isoformat(),
|
"last_updated": state.last_updated.isoformat(),
|
||||||
"attributes": dict(state.attributes)
|
"attributes": dict(state.attributes),
|
||||||
}
|
}
|
||||||
|
|
||||||
if entity_entry:
|
if entity_entry:
|
||||||
@ -143,28 +173,27 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
"conversation", {}
|
"conversation", {}
|
||||||
).get("should_expose", False)
|
).get("should_expose", False)
|
||||||
|
|
||||||
return metadata
|
return ServiceResponse(metadata)
|
||||||
|
|
||||||
# Register services
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
"get_exposed_entities",
|
"get_exposed_entities",
|
||||||
get_exposed_entities,
|
get_exposed_entities,
|
||||||
schema=None
|
supports_response=SupportsResponse.ONLY,
|
||||||
)
|
)
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
"get_exposed_scripts",
|
"get_exposed_scripts",
|
||||||
get_exposed_scripts,
|
get_exposed_scripts,
|
||||||
schema=None
|
supports_response=SupportsResponse.ONLY,
|
||||||
)
|
)
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
"get_entity_metadata",
|
"get_entity_metadata",
|
||||||
get_entity_metadata,
|
get_entity_metadata,
|
||||||
schema=None
|
supports_response=SupportsResponse.ONLY,
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER.info("MCP Bridge services registered successfully")
|
_LOGGER.info("MCP Bridge services registered successfully")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user