mirror of
https://github.com/esphome/esphome.git
synced 2026-08-22 22:26:21 +00:00
[api] Warn when Home Assistant actions are sent with no subscribed client (#17560)
This commit is contained in:
@@ -166,10 +166,14 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
#endif
|
||||
bool try_send_log_message(int level, const char *tag, const char *line, size_t message_len);
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
void send_homeassistant_action(const HomeassistantActionRequest &call) {
|
||||
// Returns whether this client has subscribed to Home Assistant actions; the message
|
||||
// is only handed to the send path when subscribed. A true return does not guarantee
|
||||
// delivery - it lets the caller warn when no connected client has the subscription.
|
||||
bool send_homeassistant_action(const HomeassistantActionRequest &call) {
|
||||
if (!this->flags_.service_call_subscription)
|
||||
return;
|
||||
return false;
|
||||
this->send_message(call);
|
||||
return true;
|
||||
}
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
void on_homeassistant_action_response(const HomeassistantActionResponse &msg);
|
||||
|
||||
@@ -426,8 +426,16 @@ void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = bat
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call) {
|
||||
bool has_subscriber = false;
|
||||
for (auto &client : this->active_clients()) {
|
||||
client->send_homeassistant_action(call);
|
||||
has_subscriber |= client->send_homeassistant_action(call);
|
||||
}
|
||||
if (!has_subscriber) {
|
||||
// Home Assistant subscribes to actions shortly *after* authenticating, so actions
|
||||
// fired right at connection time (on_client_connected, on_time_sync, ...) can
|
||||
// arrive before the subscription and are lost - warn instead of failing silently.
|
||||
ESP_LOGW(TAG, "Home Assistant %s '%s' dropped; %s", call.is_event ? "event" : "action", call.service.c_str(),
|
||||
this->is_connected() ? "client has not subscribed to actions (yet)" : "no client connected");
|
||||
}
|
||||
}
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
esphome:
|
||||
name: test-ha-action-no-subscriber
|
||||
friendly_name: Home Assistant Action No Subscriber Test
|
||||
on_boot:
|
||||
# Fires before any client is connected - dropped with a warning.
|
||||
- homeassistant.action:
|
||||
action: test.boot_action
|
||||
|
||||
host:
|
||||
|
||||
api:
|
||||
on_client_connected:
|
||||
# Fires at authentication time, before the client has subscribed to
|
||||
# Home Assistant actions - dropped with a warning.
|
||||
- homeassistant.action:
|
||||
action: test.connected_action
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: Send Action Button
|
||||
id: send_action_button
|
||||
on_press:
|
||||
# Pressed only after the client has subscribed - must be delivered.
|
||||
- homeassistant.action:
|
||||
action: test.button_action
|
||||
data:
|
||||
value: subscribed
|
||||
@@ -0,0 +1,79 @@
|
||||
"""Integration test for Home Assistant actions fired without a subscriber.
|
||||
|
||||
Home Assistant subscribes to device actions shortly after authenticating, while
|
||||
on_client_connected (and similar triggers) fire right at authentication. Actions
|
||||
fired before any client has subscribed cannot be delivered - they must produce a
|
||||
warning in the log instead of vanishing silently.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from aioesphomeapi import ButtonInfo, HomeassistantServiceCall
|
||||
import pytest
|
||||
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_homeassistant_action_no_subscriber(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Undeliverable actions warn in the log; actions after subscribing arrive."""
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
boot_warning_future = loop.create_future()
|
||||
connected_warning_future = loop.create_future()
|
||||
button_action_future = loop.create_future()
|
||||
|
||||
def check_output(line: str) -> None:
|
||||
if (
|
||||
not boot_warning_future.done()
|
||||
and "Home Assistant action 'test.boot_action' dropped; no client connected"
|
||||
in line
|
||||
):
|
||||
boot_warning_future.set_result(True)
|
||||
if (
|
||||
not connected_warning_future.done()
|
||||
and "Home Assistant action 'test.connected_action' dropped; "
|
||||
"client has not subscribed to actions (yet)"
|
||||
in line
|
||||
):
|
||||
connected_warning_future.set_result(True)
|
||||
|
||||
service_calls: list[HomeassistantServiceCall] = []
|
||||
|
||||
def on_service_call(service_call: HomeassistantServiceCall) -> None:
|
||||
service_calls.append(service_call)
|
||||
if (
|
||||
service_call.service == "test.button_action"
|
||||
and not button_action_future.done()
|
||||
):
|
||||
button_action_future.set_result(service_call)
|
||||
|
||||
async with run_compiled(yaml_config, line_callback=check_output):
|
||||
# The on_boot action fires with no client connected at all.
|
||||
await asyncio.wait_for(boot_warning_future, timeout=10.0)
|
||||
|
||||
async with api_client_connected() as client:
|
||||
device_info = await client.device_info()
|
||||
assert device_info.name == "test-ha-action-no-subscriber"
|
||||
|
||||
# on_client_connected fired at authentication, before this client
|
||||
# subscribed to Home Assistant actions.
|
||||
await asyncio.wait_for(connected_warning_future, timeout=5.0)
|
||||
|
||||
# After subscribing, actions must be delivered normally (and the
|
||||
# dropped ones must not suddenly show up).
|
||||
client.subscribe_service_calls(on_service_call)
|
||||
|
||||
entities, _ = await client.list_entities_services()
|
||||
button = next(e for e in entities if isinstance(e, ButtonInfo))
|
||||
client.button_command(button.key)
|
||||
|
||||
button_call = await asyncio.wait_for(button_action_future, timeout=5.0)
|
||||
assert button_call.data == {"value": "subscribed"}
|
||||
assert [call.service for call in service_calls] == ["test.button_action"]
|
||||
Reference in New Issue
Block a user