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
129 changes: 124 additions & 5 deletions homeassistant/components/esphome/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,18 @@
State,
callback,
)
from homeassistant.exceptions import HomeAssistantError, TemplateError
from homeassistant.exceptions import (
HomeAssistantError,
ServiceNotFound,
ServiceValidationError,
TemplateError,
)
from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
entity_registry as er,
issue_registry as ir,
json,
template,
)
from homeassistant.helpers.device_registry import format_mac
Expand Down Expand Up @@ -273,11 +279,32 @@ def async_on_service_call(self, service: HomeassistantServiceCall) -> None:
elif self.entry.options.get(
CONF_ALLOW_SERVICE_CALLS, DEFAULT_ALLOW_SERVICE_CALLS
):
hass.async_create_task(
hass.services.async_call(
domain, service_name, service_data, blocking=True
call_id = service.call_id
if call_id and service.wants_response:
# Service call with response expected
self.entry.async_create_task(
hass,
self._handle_service_call_with_response(
domain,
service_name,
service_data,
call_id,
service.response_template,
),
)
elif call_id:
# Service call without response but needs success/failure notification
self.entry.async_create_task(
hass,
self._handle_service_call_with_notification(
domain, service_name, service_data, call_id
),
)
else:
# Fire and forget service call
self.entry.async_create_task(
hass, hass.services.async_call(domain, service_name, service_data)
)
)
else:
device_info = self.entry_data.device_info
assert device_info is not None
Expand All @@ -303,6 +330,98 @@ def async_on_service_call(self, service: HomeassistantServiceCall) -> None:
service_data,
)

async def _handle_service_call_with_response(
self,
domain: str,
service_name: str,
service_data: dict,
call_id: int,
response_template: str | None = None,
) -> None:
"""Handle service call that expects a response and send response back to ESPHome."""
try:
# Call the service with response capture enabled
action_response = await self.hass.services.async_call(
domain=domain,
service=service_name,
service_data=service_data,
blocking=True,
return_response=True,
)

if response_template:
try:
# Render response template
tmpl = Template(response_template, self.hass)
response = tmpl.async_render(
variables={"response": action_response},
strict=True,
)
response_dict = {"response": response}

except TemplateError as ex:
raise HomeAssistantError(
f"Error rendering response template: {ex}"
) from ex
else:
response_dict = {"response": action_response}

# JSON encode response data for ESPHome
response_data = json.json_bytes(response_dict)

except (
ServiceNotFound,
ServiceValidationError,
vol.Invalid,
HomeAssistantError,
) as ex:
self._send_service_call_response(
call_id, success=False, error_message=str(ex), response_data=b""
)

else:
# Send success response back to ESPHome
self._send_service_call_response(
call_id=call_id,
success=True,
error_message="",
response_data=response_data,
)

async def _handle_service_call_with_notification(
self, domain: str, service_name: str, service_data: dict, call_id: int
) -> None:
"""Handle service call that needs success/failure notification."""
try:
await self.hass.services.async_call(
domain, service_name, service_data, blocking=True
)
except (ServiceNotFound, ServiceValidationError, vol.Invalid) as ex:
self._send_service_call_response(call_id, False, str(ex), b"")
else:
self._send_service_call_response(call_id, True, "", b"")

def _send_service_call_response(
self,
call_id: int,
success: bool,
error_message: str,
response_data: bytes,
) -> None:
"""Send service call response back to ESPHome device."""
_LOGGER.debug(
"Service call response for call_id %s: success=%s, error=%s",
call_id,
success,
error_message,
)
self.cli.send_homeassistant_action_response(
call_id,
success,
error_message,
response_data,
)

@callback
def _send_home_assistant_state(
self, entity_id: str, attribute: str | None, state: State | None
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/esphome/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"mqtt": ["esphome/discover/#"],
"quality_scale": "platinum",
"requirements": [
"aioesphomeapi==42.0.0",
"aioesphomeapi==42.1.0",
"esphome-dashboard-api==1.3.0",
"bleak-esphome==3.4.0"
],
Expand Down
15 changes: 9 additions & 6 deletions homeassistant/components/xbox/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from __future__ import annotations

from contextlib import suppress
from dataclasses import dataclass
from datetime import timedelta
import logging
Expand Down Expand Up @@ -154,10 +153,14 @@ async def _async_update_data(self) -> XboxData:
def _build_presence_data(person: Person) -> PresenceData:
"""Build presence data from a person."""
active_app: PresenceDetail | None = None
with suppress(StopIteration):
active_app = next(
presence for presence in person.presence_details if presence.is_primary
)

active_app = next(
(presence for presence in person.presence_details if presence.is_primary),
None,
)
in_game = (
active_app is not None and active_app.is_game and active_app.state == "Active"
)

return PresenceData(
xuid=person.xuid,
Expand All @@ -166,7 +169,7 @@ def _build_presence_data(person: Person) -> PresenceData:
online=person.presence_state == "Online",
status=person.presence_text,
in_party=person.multiplayer_summary.in_party > 0,
in_game=active_app is not None and active_app.is_game,
in_game=in_game,
in_multiplayer=person.multiplayer_summary.in_multiplayer_session,
gamer_score=person.gamer_score,
gold_tenure=person.detail.tenure,
Expand Down
2 changes: 1 addition & 1 deletion requirements_all.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion requirements_test_all.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading