fix: Services survive config entry reload, get_history returns a dict response

- service registration extracted to idempotent _async_register_services,
  called from both async_setup and async_setup_entry: unloading the last
  entry unregisters services, and a reload (every options change) never
  re-ran async_setup, leaving the integration without services
- get_history handler wraps the list in {"history": [...]}: HA rejects
  non-dict action responses, so every return_response call failed with a
  server error since the service gained SupportsResponse (v2.4.x) - the
  path never worked, no consumer could depend on the old shape
- README: get_history example shows response_variable usage and the
  actual limit clamp semantics

Both found by live smoke test in HA 2026.7 (Docker), not by static review
This commit is contained in:
SMKRV
2026-07-07 01:38:27 +03:00
parent bcfe69b5f7
commit fc59f584a3
2 changed files with 24 additions and 5 deletions
+2 -1
View File
@@ -283,12 +283,13 @@ data:
```yaml ```yaml
service: ha_text_ai.get_history service: ha_text_ai.get_history
data: data:
limit: 5 # optional, number of conversations to return (1-100) limit: 5 # optional, number of conversations to return (values above 200 are clamped); omit to get the full stored history
filter_model: "gpt-4o" # optional, filter by specific AI model filter_model: "gpt-4o" # optional, filter by specific AI model
start_date: "2025-02-01" # optional, filter conversations from this date start_date: "2025-02-01" # optional, filter conversations from this date
include_metadata: false # optional, include tokens, response time, etc. include_metadata: false # optional, include tokens, response time, etc.
sort_order: "newest" # optional, sort order: "newest" or "oldest" sort_order: "newest" # optional, sort order: "newest" or "oldest"
instance: sensor.ha_text_ai_gpt instance: sensor.ha_text_ai_gpt
response_variable: history_result # entries are in history_result.history
``` ```
## 🚀 Advanced Automation Examples with Response Variables ## 🚀 Advanced Automation Examples with Response Variables
+22 -4
View File
@@ -116,6 +116,19 @@ async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool:
"""Set up the Home Assistant Text AI component.""" """Set up the Home Assistant Text AI component."""
# Initialize domain data storage # Initialize domain data storage
hass.data.setdefault(DOMAIN, {}) hass.data.setdefault(DOMAIN, {})
_async_register_services(hass)
return True
def _async_register_services(hass: HomeAssistant) -> None:
"""Register domain services; safe to call again after unload.
Unloading the last config entry unregisters the services, and a config
entry reload (every options change does one) runs unload + setup_entry
without re-running async_setup — so setup_entry must be able to bring
the services back.
"""
if hass.services.has_service(DOMAIN, SERVICE_ASK_QUESTION):
return
async def async_ask_question(call: ServiceCall) -> dict: async def async_ask_question(call: ServiceCall) -> dict:
"""Handle ask_question service with response data.""" """Handle ask_question service with response data."""
@@ -171,17 +184,21 @@ async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool:
_LOGGER.error("Error clearing history: %s", str(err)) _LOGGER.error("Error clearing history: %s", str(err))
raise HomeAssistantError(f"Failed to clear history: {str(err)}") from err raise HomeAssistantError(f"Failed to clear history: {str(err)}") from err
async def async_get_history(call: ServiceCall) -> list: async def async_get_history(call: ServiceCall) -> dict:
"""Handle get_history service.""" """Handle get_history service."""
try: try:
coordinator = get_coordinator_by_instance(hass, call.data["instance"]) coordinator = get_coordinator_by_instance(hass, call.data["instance"])
return await coordinator.async_get_history( history = await coordinator.async_get_history(
limit=call.data.get("limit"), limit=call.data.get("limit"),
filter_model=call.data.get("filter_model"), filter_model=call.data.get("filter_model"),
start_date=call.data.get("start_date"), start_date=call.data.get("start_date"),
include_metadata=call.data.get("include_metadata", False), include_metadata=call.data.get("include_metadata", False),
sort_order=call.data.get("sort_order", "newest") sort_order=call.data.get("sort_order", "newest")
) )
# HA requires action responses to be dicts. The bare list made
# every return_response call fail with a server error, so this
# path never worked before and the wrapper breaks no consumer.
return {"history": history}
except Exception as err: except Exception as err:
_LOGGER.error("Error getting history: %s", str(err)) _LOGGER.error("Error getting history: %s", str(err))
raise HomeAssistantError(f"Failed to get history: {str(err)}") from err raise HomeAssistantError(f"Failed to get history: {str(err)}") from err
@@ -226,8 +243,6 @@ async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool:
schema=SERVICE_SCHEMA_SET_SYSTEM_PROMPT schema=SERVICE_SCHEMA_SET_SYSTEM_PROMPT
) )
return True
async def async_check_api(session, endpoint: str, headers: dict, provider: str, api_timeout: int = DEFAULT_API_TIMEOUT) -> bool: async def async_check_api(session, endpoint: str, headers: dict, provider: str, api_timeout: int = DEFAULT_API_TIMEOUT) -> bool:
"""Check API availability using provider registry configuration.""" """Check API availability using provider registry configuration."""
try: try:
@@ -353,6 +368,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data.setdefault(DOMAIN, {}) hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinator hass.data[DOMAIN][entry.entry_id] = coordinator
# A reload after the last entry was unloaded needs the services back.
_async_register_services(hass)
_LOGGER.debug("Stored coordinator in hass.data[%s][%s]", DOMAIN, entry.entry_id) _LOGGER.debug("Stored coordinator in hass.data[%s][%s]", DOMAIN, entry.entry_id)
# Set up platforms # Set up platforms