feat: inject device area context for room-aware voice responses#12
feat: inject device area context for room-aware voice responses#12DarrenBenson wants to merge 16 commits intotechartdev:mainfrom
Conversation
Partial/incomplete debug logging references (CONF_DEBUG_LOGGING, broken options.get() calls) were accidentally included in this branch. The full debug logging feature lives in feature/debug-logging.
When the assistant's response ends with a question mark or contains common follow-up patterns (EN/DE), set continue_conversation=True on the ConversationResult. This tells Voice PE and other HA voice satellites to automatically re-listen after the response finishes playing, enabling natural back-and-forth dialog without requiring the wake word between turns. Fixes techartdev#7
Resolves the originating voice satellite's area from the HA device registry and injects it as "[Voice command from: <room>]" in the system prompt sent to OpenClaw. Also passes x-openclaw-area and x-openclaw-device-id headers for structured access. This enables room-aware voice responses -- the agent knows which room the user is speaking from without being told. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When no area is found (no device_id, device has no area, etc.), the request is now identical to the original unmodified code -- no extra headers, no area context in the system prompt. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The device ID is useful on its own for logging, debugging, and future device-specific logic. Area header is still only sent when the area is successfully resolved. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds room-awareness to OpenClaw Assist requests by resolving the originating voice device’s area and passing it along in both prompt context and HTTP headers.
Changes:
- Resolve
device_id→area_id→ area name via HA device/area registries. - Prepend
[Voice command from: <room>]to the system prompt when available. - Send
x-openclaw-device-idandx-openclaw-areaheaders to OpenClaw; extend_get_responseto accept per-request headers.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if device_area_context: | ||
| area_name = device_area_context.removeprefix("[Voice command from: ").removesuffix("]") | ||
| voice_headers["x-openclaw-area"] = area_name |
There was a problem hiding this comment.
Extracting area_name by parsing device_area_context (removeprefix/removesuffix) is brittle because any future change to the prompt wording or formatting will silently break the header value. Prefer having _resolve_device_area return the raw area_name (or a small dataclass/tuple like (area_name, context_line)) and build both the system prompt line and the x-openclaw-area header from that.
| extra_headers: dict[str, str] | None = None, | ||
| ) -> str: | ||
| """Get a response from OpenClaw, trying streaming first.""" | ||
| headers = extra_headers or _VOICE_REQUEST_HEADERS |
There was a problem hiding this comment.
headers = extra_headers or _VOICE_REQUEST_HEADERS treats extra_headers as a full replacement, even though the parameter name suggests these are additional headers. To avoid accidentally dropping the voice routing headers in future call sites, consider always merging with the defaults (e.g., start from _VOICE_REQUEST_HEADERS and update with extra_headers when provided).
| headers = extra_headers or _VOICE_REQUEST_HEADERS | |
| headers: dict[str, str] = dict(_VOICE_REQUEST_HEADERS) | |
| if extra_headers: | |
| headers.update(extra_headers) |
|
|
||
| return f"[Voice command from: {area_entry.name}]" | ||
| except Exception: | ||
| _LOGGER.debug("Could not resolve area for device %s", device_id) |
There was a problem hiding this comment.
Catching a broad Exception here can hide unexpected bugs (e.g., registry API changes) and the log message drops the stack trace. Consider narrowing the exception types you expect from the registry lookups and/or logging with exc_info=True so failures can be diagnosed from logs without reproducing locally.
| _LOGGER.debug("Could not resolve area for device %s", device_id) | |
| _LOGGER.debug("Could not resolve area for device %s", device_id, exc_info=True) |
| if device_area_context: | ||
| area_name = device_area_context.removeprefix("[Voice command from: ").removesuffix("]") | ||
| voice_headers["x-openclaw-area"] = area_name |
There was a problem hiding this comment.
area_name is derived from device_area_context and then sent as an HTTP header value. Because area_entry.name is user-controlled, it should be sanitized to prevent CRLF/newline characters (which can cause aiohttp header validation errors or enable header injection) and to keep the injected system-prompt line single-line. Consider deriving area_name directly from the registry lookup and normalizing it (strip, replace \r/\n with spaces, and optionally enforce a reasonable max length) before using it in both the prompt and headers.
Merges dalehamel's fix for agent ID routing (issue techartdev#8): - Agent ID sent as model field (openclaw:<agent_id>) for gateway routing - Sticky sessions persisted to disk, durable across HA restarts - Resolved agent ID used consistently (voice_agent_id || configured_agent_id) Merged on top of our room awareness feature (device area injection). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds opt-in CONF_DEBUG_LOGGING toggle for API request tracing. Enhanced debug log to include resolved area name alongside agent ID and session ID. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tier 1 improvements from code review: - Extract _extract_text_recursive and _normalize_optional_text to shared utils.py module (was duplicated in __init__.py and conversation.py) - Map OpenClawConnectionError/AuthError to FAILED_TO_HANDLE instead of UNKNOWN, helping HA's Assist pipeline make smarter fallback decisions - Fix api.py session fallback to log warnings when HA-managed session is unexpectedly closed instead of silently creating orphan sessions - Merge PR techartdev#11 continue_conversation for Voice PE follow-up dialog - Fix regex syntax warning in _should_continue Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tier 2 improvements from code review: - exposure.py: include area assignments, useful state attributes (brightness, temperature, volume, etc.), and current date/time in the entity context sent to OpenClaw - api.py: add async_send_message_with_retry() with 2 retries and 1s delay for transient connection failures (skips auth errors) - config_flow.py: document _find_addon_config_dir as blocking I/O (already correctly wrapped in async_add_executor_job by caller) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace upstream README with fork-specific documentation covering room-aware voice, richer entity context, merged community PRs, and code quality improvements. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PR techartdev#9 debug logging added _log_request() but referenced undefined variables (agent_id, payload, session_id, headers) in the async_check_connection method, causing 500 errors during config flow. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ky sessions) The static session ID override is no longer needed now that PR techartdev#10 provides durable, agent-scoped sticky sessions persisted to disk. The old option caused confusion by routing all requests to whichever agent owned the named session, bypassing voice agent routing. Removed from: config_flow, conversation, strings, translations. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
[Voice command from: <room>]into the system prompt sent to OpenClawx-openclaw-areaandx-openclaw-device-idheaders for structured accessMotivation
When a user speaks to a voice satellite (e.g. Voice PE in the Study), OpenClaw has no way to know which room they're in. This means "turn off the lights" or "what's the temperature in here?" can't be resolved to the correct room.
HA's
ConversationInputalready carriesdevice_id(since HA 2024.4), and the device registry knows which area each device belongs to. This PR connects those dots.How it works
device_idfromConversationInput(already provided by the Assist pipeline)hass.helpers.device_registryto get itsarea_idhass.helpers.area_registryto get the area name[Voice command from: Study]to the system promptx-openclaw-areaandx-openclaw-device-idheadersBackwards compatible
device_idis not available (e.g. non-satellite input), no context is addedx-openclaw-device-idis sentTest plan
🤖 Generated with Claude Code