Speed up single entity/response service calls (#96729)

* Significantly speed up single entity/response service calls

Since the majority of service calls are single entity, we can
avoid creating tasks in this case. Since the multi-entity
service calls always check the result and raise, we can switch
the asyncio.wait to asyncio.gather

* Significantly speed up single entity/response service calls

Since the majority of service calls are single entity, we can
avoid creating tasks in this case. Since the multi-entity
service calls always check the result and raise, we can switch
the asyncio.wait to asyncio.gather

* revert

* cannot be inside pytest.raises

* one more

* Update homeassistant/helpers/service.py
This commit is contained in:
J. Nick Koston 2023-07-16 21:33:13 -10:00 committed by GitHub
parent c76fac0633
commit 3a06659120
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 50 additions and 18 deletions

View file

@ -741,6 +741,8 @@ async def entity_service_call( # noqa: C901
Calls all platforms simultaneously. Calls all platforms simultaneously.
""" """
entity_perms: None | (Callable[[str, str], bool]) = None entity_perms: None | (Callable[[str, str], bool]) = None
return_response = call.return_response
if call.context.user_id: if call.context.user_id:
user = await hass.auth.async_get_user(call.context.user_id) user = await hass.auth.async_get_user(call.context.user_id)
if user is None: if user is None:
@ -851,13 +853,27 @@ async def entity_service_call( # noqa: C901
entities.append(entity) entities.append(entity)
if not entities: if not entities:
if call.return_response: if return_response:
raise HomeAssistantError( raise HomeAssistantError(
"Service call requested response data but did not match any entities" "Service call requested response data but did not match any entities"
) )
return None return None
if call.return_response and len(entities) != 1: if len(entities) == 1:
# Single entity case avoids creating tasks and allows returning
# ServiceResponse
entity = entities[0]
response_data = await _handle_entity_call(
hass, entity, func, data, call.context
)
if entity.should_poll:
# Context expires if the turn on commands took a long time.
# Set context again so it's there when we update
entity.async_set_context(call.context)
await entity.async_update_ha_state(True)
return response_data if return_response else None
if return_response:
raise HomeAssistantError( raise HomeAssistantError(
"Service call requested response data but matched more than one entity" "Service call requested response data but matched more than one entity"
) )
@ -874,9 +890,8 @@ async def entity_service_call( # noqa: C901
) )
assert not pending assert not pending
response_data: ServiceResponse | None
for task in done: for task in done:
response_data = task.result() # pop exception if have task.result() # pop exception if have
tasks: list[asyncio.Task[None]] = [] tasks: list[asyncio.Task[None]] = []
@ -895,7 +910,7 @@ async def entity_service_call( # noqa: C901
for future in done: for future in done:
future.result() # pop exception if have future.result() # pop exception if have
return response_data if call.return_response else None return None
async def _handle_entity_call( async def _handle_entity_call(

View file

@ -228,7 +228,8 @@ async def test_auth_failed(hass: HomeAssistant, mock_device: MockDevice) -> None
{ATTR_ENTITY_ID: state_key}, {ATTR_ENTITY_ID: state_key},
blocking=True, blocking=True,
) )
await hass.async_block_till_done()
await hass.async_block_till_done()
flows = hass.config_entries.flow.async_progress() flows = hass.config_entries.flow.async_progress()
assert len(flows) == 1 assert len(flows) == 1

View file

@ -307,6 +307,9 @@ async def test_auth_failed(
await hass.services.async_call( await hass.services.async_call(
PLATFORM, SERVICE_TURN_ON, {"entity_id": state_key}, blocking=True PLATFORM, SERVICE_TURN_ON, {"entity_id": state_key}, blocking=True
) )
await hass.async_block_till_done()
flows = hass.config_entries.flow.async_progress() flows = hass.config_entries.flow.async_progress()
assert len(flows) == 1 assert len(flows) == 1

View file

@ -102,6 +102,7 @@ async def test_if_fires_using_at_input_datetime(
}, },
blocking=True, blocking=True,
) )
await hass.async_block_till_done()
time_that_will_not_match_right_away = trigger_dt - timedelta(minutes=1) time_that_will_not_match_right_away = trigger_dt - timedelta(minutes=1)
@ -148,6 +149,7 @@ async def test_if_fires_using_at_input_datetime(
}, },
blocking=True, blocking=True,
) )
await hass.async_block_till_done()
async_fire_time_changed(hass, trigger_dt + timedelta(seconds=1)) async_fire_time_changed(hass, trigger_dt + timedelta(seconds=1))
await hass.async_block_till_done() await hass.async_block_till_done()
@ -556,6 +558,7 @@ async def test_datetime_in_past_on_load(hass: HomeAssistant, calls) -> None:
}, },
blocking=True, blocking=True,
) )
await hass.async_block_till_done()
assert await async_setup_component( assert await async_setup_component(
hass, hass,
@ -587,6 +590,7 @@ async def test_datetime_in_past_on_load(hass: HomeAssistant, calls) -> None:
}, },
blocking=True, blocking=True,
) )
await hass.async_block_till_done()
async_fire_time_changed(hass, future + timedelta(seconds=1)) async_fire_time_changed(hass, future + timedelta(seconds=1))
await hass.async_block_till_done() await hass.async_block_till_done()

View file

@ -285,11 +285,13 @@ async def test_signal_repetitions_cancelling(hass: HomeAssistant, monkeypatch) -
await hass.services.async_call( await hass.services.async_call(
DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: f"{DOMAIN}.test"} DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: f"{DOMAIN}.test"}
) )
# Get background service time to start running # Get background service time to start running
await asyncio.sleep(0) await asyncio.sleep(0)
await hass.services.async_call( await hass.services.async_call(
DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: f"{DOMAIN}.test"}, blocking=True DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: f"{DOMAIN}.test"}, blocking=True
) )
await hass.async_block_till_done()
assert [call[0][1] for call in protocol.send_command_ack.call_args_list] == [ assert [call[0][1] for call in protocol.send_command_ack.call_args_list] == [
"off", "off",

View file

@ -427,6 +427,7 @@ async def test_block_set_mode_auth_error(
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.HEAT}, {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVACMode.HEAT},
blocking=True, blocking=True,
) )
await hass.async_block_till_done()
assert entry.state == ConfigEntryState.LOADED assert entry.state == ConfigEntryState.LOADED

View file

@ -186,6 +186,7 @@ async def test_block_set_value_auth_error(
{ATTR_ENTITY_ID: "number.test_name_valve_position", ATTR_VALUE: 30}, {ATTR_ENTITY_ID: "number.test_name_valve_position", ATTR_VALUE: 30},
blocking=True, blocking=True,
) )
await hass.async_block_till_done()
assert entry.state == ConfigEntryState.LOADED assert entry.state == ConfigEntryState.LOADED

View file

@ -82,6 +82,7 @@ async def test_block_set_state_auth_error(
{ATTR_ENTITY_ID: "switch.test_name_channel_1"}, {ATTR_ENTITY_ID: "switch.test_name_channel_1"},
blocking=True, blocking=True,
) )
await hass.async_block_till_done()
assert entry.state == ConfigEntryState.LOADED assert entry.state == ConfigEntryState.LOADED
@ -211,6 +212,7 @@ async def test_rpc_auth_error(
{ATTR_ENTITY_ID: "switch.test_switch_0"}, {ATTR_ENTITY_ID: "switch.test_switch_0"},
blocking=True, blocking=True,
) )
await hass.async_block_till_done()
assert entry.state == ConfigEntryState.LOADED assert entry.state == ConfigEntryState.LOADED

View file

@ -203,6 +203,7 @@ async def test_block_update_auth_error(
{ATTR_ENTITY_ID: "update.test_name_firmware_update"}, {ATTR_ENTITY_ID: "update.test_name_firmware_update"},
blocking=True, blocking=True,
) )
await hass.async_block_till_done()
assert entry.state == ConfigEntryState.LOADED assert entry.state == ConfigEntryState.LOADED
@ -541,6 +542,7 @@ async def test_rpc_update_auth_error(
blocking=True, blocking=True,
) )
await hass.async_block_till_done()
assert entry.state == ConfigEntryState.LOADED assert entry.state == ConfigEntryState.LOADED
flows = hass.config_entries.flow.async_progress() flows = hass.config_entries.flow.async_progress()

View file

@ -129,7 +129,7 @@ async def test_arm_home_failure(hass: HomeAssistant) -> None:
await hass.services.async_call( await hass.services.async_call(
ALARM_DOMAIN, SERVICE_ALARM_ARM_HOME, DATA, blocking=True ALARM_DOMAIN, SERVICE_ALARM_ARM_HOME, DATA, blocking=True
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert f"{err.value}" == "TotalConnect failed to arm home test." assert f"{err.value}" == "TotalConnect failed to arm home test."
assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED
assert mock_request.call_count == 2 assert mock_request.call_count == 2
@ -139,7 +139,7 @@ async def test_arm_home_failure(hass: HomeAssistant) -> None:
await hass.services.async_call( await hass.services.async_call(
ALARM_DOMAIN, SERVICE_ALARM_ARM_HOME, DATA, blocking=True ALARM_DOMAIN, SERVICE_ALARM_ARM_HOME, DATA, blocking=True
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert f"{err.value}" == "TotalConnect usercode is invalid. Did not arm home" assert f"{err.value}" == "TotalConnect usercode is invalid. Did not arm home"
assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED
# should have started a re-auth flow # should have started a re-auth flow
@ -183,7 +183,7 @@ async def test_arm_home_instant_failure(hass: HomeAssistant) -> None:
await hass.services.async_call( await hass.services.async_call(
DOMAIN, SERVICE_ALARM_ARM_HOME_INSTANT, DATA, blocking=True DOMAIN, SERVICE_ALARM_ARM_HOME_INSTANT, DATA, blocking=True
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert f"{err.value}" == "TotalConnect failed to arm home instant test." assert f"{err.value}" == "TotalConnect failed to arm home instant test."
assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED
assert mock_request.call_count == 2 assert mock_request.call_count == 2
@ -193,7 +193,7 @@ async def test_arm_home_instant_failure(hass: HomeAssistant) -> None:
await hass.services.async_call( await hass.services.async_call(
DOMAIN, SERVICE_ALARM_ARM_HOME_INSTANT, DATA, blocking=True DOMAIN, SERVICE_ALARM_ARM_HOME_INSTANT, DATA, blocking=True
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert ( assert (
f"{err.value}" f"{err.value}"
== "TotalConnect usercode is invalid. Did not arm home instant" == "TotalConnect usercode is invalid. Did not arm home instant"
@ -240,7 +240,7 @@ async def test_arm_away_instant_failure(hass: HomeAssistant) -> None:
await hass.services.async_call( await hass.services.async_call(
DOMAIN, SERVICE_ALARM_ARM_AWAY_INSTANT, DATA, blocking=True DOMAIN, SERVICE_ALARM_ARM_AWAY_INSTANT, DATA, blocking=True
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert f"{err.value}" == "TotalConnect failed to arm away instant test." assert f"{err.value}" == "TotalConnect failed to arm away instant test."
assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED
assert mock_request.call_count == 2 assert mock_request.call_count == 2
@ -250,7 +250,7 @@ async def test_arm_away_instant_failure(hass: HomeAssistant) -> None:
await hass.services.async_call( await hass.services.async_call(
DOMAIN, SERVICE_ALARM_ARM_AWAY_INSTANT, DATA, blocking=True DOMAIN, SERVICE_ALARM_ARM_AWAY_INSTANT, DATA, blocking=True
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert ( assert (
f"{err.value}" f"{err.value}"
== "TotalConnect usercode is invalid. Did not arm away instant" == "TotalConnect usercode is invalid. Did not arm away instant"
@ -296,7 +296,7 @@ async def test_arm_away_failure(hass: HomeAssistant) -> None:
await hass.services.async_call( await hass.services.async_call(
ALARM_DOMAIN, SERVICE_ALARM_ARM_AWAY, DATA, blocking=True ALARM_DOMAIN, SERVICE_ALARM_ARM_AWAY, DATA, blocking=True
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert f"{err.value}" == "TotalConnect failed to arm away test." assert f"{err.value}" == "TotalConnect failed to arm away test."
assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED
assert mock_request.call_count == 2 assert mock_request.call_count == 2
@ -306,7 +306,7 @@ async def test_arm_away_failure(hass: HomeAssistant) -> None:
await hass.services.async_call( await hass.services.async_call(
ALARM_DOMAIN, SERVICE_ALARM_ARM_AWAY, DATA, blocking=True ALARM_DOMAIN, SERVICE_ALARM_ARM_AWAY, DATA, blocking=True
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert f"{err.value}" == "TotalConnect usercode is invalid. Did not arm away" assert f"{err.value}" == "TotalConnect usercode is invalid. Did not arm away"
assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED
# should have started a re-auth flow # should have started a re-auth flow
@ -353,7 +353,7 @@ async def test_disarm_failure(hass: HomeAssistant) -> None:
await hass.services.async_call( await hass.services.async_call(
ALARM_DOMAIN, SERVICE_ALARM_DISARM, DATA, blocking=True ALARM_DOMAIN, SERVICE_ALARM_DISARM, DATA, blocking=True
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert f"{err.value}" == "TotalConnect failed to disarm test." assert f"{err.value}" == "TotalConnect failed to disarm test."
assert hass.states.get(ENTITY_ID).state == STATE_ALARM_ARMED_AWAY assert hass.states.get(ENTITY_ID).state == STATE_ALARM_ARMED_AWAY
assert mock_request.call_count == 2 assert mock_request.call_count == 2
@ -363,7 +363,7 @@ async def test_disarm_failure(hass: HomeAssistant) -> None:
await hass.services.async_call( await hass.services.async_call(
ALARM_DOMAIN, SERVICE_ALARM_DISARM, DATA, blocking=True ALARM_DOMAIN, SERVICE_ALARM_DISARM, DATA, blocking=True
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert f"{err.value}" == "TotalConnect usercode is invalid. Did not disarm" assert f"{err.value}" == "TotalConnect usercode is invalid. Did not disarm"
assert hass.states.get(ENTITY_ID).state == STATE_ALARM_ARMED_AWAY assert hass.states.get(ENTITY_ID).state == STATE_ALARM_ARMED_AWAY
# should have started a re-auth flow # should have started a re-auth flow
@ -406,7 +406,7 @@ async def test_arm_night_failure(hass: HomeAssistant) -> None:
await hass.services.async_call( await hass.services.async_call(
ALARM_DOMAIN, SERVICE_ALARM_ARM_NIGHT, DATA, blocking=True ALARM_DOMAIN, SERVICE_ALARM_ARM_NIGHT, DATA, blocking=True
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert f"{err.value}" == "TotalConnect failed to arm night test." assert f"{err.value}" == "TotalConnect failed to arm night test."
assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED
assert mock_request.call_count == 2 assert mock_request.call_count == 2
@ -416,7 +416,7 @@ async def test_arm_night_failure(hass: HomeAssistant) -> None:
await hass.services.async_call( await hass.services.async_call(
ALARM_DOMAIN, SERVICE_ALARM_ARM_NIGHT, DATA, blocking=True ALARM_DOMAIN, SERVICE_ALARM_ARM_NIGHT, DATA, blocking=True
) )
await hass.async_block_till_done() await hass.async_block_till_done()
assert f"{err.value}" == "TotalConnect usercode is invalid. Did not arm night" assert f"{err.value}" == "TotalConnect usercode is invalid. Did not arm night"
assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED
# should have started a re-auth flow # should have started a re-auth flow

View file

@ -456,6 +456,7 @@ async def test_options_update(
options=new_options, options=new_options,
) )
assert config_entry.options == updated_options assert config_entry.options == updated_options
await hass.async_block_till_done()
await _test_service( await _test_service(
hass, MP_DOMAIN, "vol_up", SERVICE_VOLUME_UP, None, num=VOLUME_STEP hass, MP_DOMAIN, "vol_up", SERVICE_VOLUME_UP, None, num=VOLUME_STEP
) )