Batch Google Report State (#49511)
* Batch Google Report State * Fix batching
This commit is contained in:
parent
c6edc7ae4f
commit
a6d87b7fae
2 changed files with 85 additions and 12 deletions
|
@ -1,8 +1,11 @@
|
||||||
"""Google Report State implementation."""
|
"""Google Report State implementation."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections import deque
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.const import MATCH_ALL
|
from homeassistant.const import MATCH_ALL
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback
|
||||||
from homeassistant.helpers.event import async_call_later
|
from homeassistant.helpers.event import async_call_later
|
||||||
from homeassistant.helpers.significant_change import create_checker
|
from homeassistant.helpers.significant_change import create_checker
|
||||||
|
|
||||||
|
@ -14,6 +17,8 @@ from .helpers import AbstractConfig, GoogleEntity, async_get_entities
|
||||||
# https://github.com/actions-on-google/smart-home-nodejs/issues/196#issuecomment-439156639
|
# https://github.com/actions-on-google/smart-home-nodejs/issues/196#issuecomment-439156639
|
||||||
INITIAL_REPORT_DELAY = 60
|
INITIAL_REPORT_DELAY = 60
|
||||||
|
|
||||||
|
# Seconds to wait to group states
|
||||||
|
REPORT_STATE_WINDOW = 1
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -22,8 +27,35 @@ _LOGGER = logging.getLogger(__name__)
|
||||||
def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig):
|
def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig):
|
||||||
"""Enable state reporting."""
|
"""Enable state reporting."""
|
||||||
checker = None
|
checker = None
|
||||||
|
unsub_pending: CALLBACK_TYPE | None = None
|
||||||
|
pending = deque([{}])
|
||||||
|
|
||||||
|
async def report_states(now=None):
|
||||||
|
"""Report the states."""
|
||||||
|
nonlocal pending
|
||||||
|
nonlocal unsub_pending
|
||||||
|
|
||||||
|
pending.append({})
|
||||||
|
|
||||||
|
# We will report all batches except last one because those are finalized.
|
||||||
|
while len(pending) > 1:
|
||||||
|
await google_config.async_report_state_all(
|
||||||
|
{"devices": {"states": pending.popleft()}}
|
||||||
|
)
|
||||||
|
|
||||||
|
# If things got queued up in last batch while we were reporting, schedule ourselves again
|
||||||
|
if pending[0]:
|
||||||
|
unsub_pending = async_call_later(
|
||||||
|
hass, REPORT_STATE_WINDOW, report_states_job
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
unsub_pending = None
|
||||||
|
|
||||||
|
report_states_job = HassJob(report_states)
|
||||||
|
|
||||||
async def async_entity_state_listener(changed_entity, old_state, new_state):
|
async def async_entity_state_listener(changed_entity, old_state, new_state):
|
||||||
|
nonlocal unsub_pending
|
||||||
|
|
||||||
if not hass.is_running:
|
if not hass.is_running:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -47,11 +79,19 @@ def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig
|
||||||
if not checker.async_is_significant_change(new_state, extra_arg=entity_data):
|
if not checker.async_is_significant_change(new_state, extra_arg=entity_data):
|
||||||
return
|
return
|
||||||
|
|
||||||
_LOGGER.debug("Reporting state for %s: %s", changed_entity, entity_data)
|
_LOGGER.debug("Scheduling report state for %s: %s", changed_entity, entity_data)
|
||||||
|
|
||||||
await google_config.async_report_state_all(
|
# If a significant change is already scheduled and we have another significant one,
|
||||||
{"devices": {"states": {changed_entity: entity_data}}}
|
# let's create a new batch of changes
|
||||||
)
|
if changed_entity in pending[-1]:
|
||||||
|
pending.append({})
|
||||||
|
|
||||||
|
pending[-1][changed_entity] = entity_data
|
||||||
|
|
||||||
|
if unsub_pending is None:
|
||||||
|
unsub_pending = async_call_later(
|
||||||
|
hass, REPORT_STATE_WINDOW, report_states_job
|
||||||
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def extra_significant_check(
|
def extra_significant_check(
|
||||||
|
@ -102,5 +142,10 @@ def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig
|
||||||
|
|
||||||
unsub = async_call_later(hass, INITIAL_REPORT_DELAY, inital_report)
|
unsub = async_call_later(hass, INITIAL_REPORT_DELAY, inital_report)
|
||||||
|
|
||||||
# pylint: disable=unnecessary-lambda
|
@callback
|
||||||
return lambda: unsub()
|
def unsub_all():
|
||||||
|
unsub()
|
||||||
|
if unsub_pending:
|
||||||
|
unsub_pending() # pylint: disable=not-callable
|
||||||
|
|
||||||
|
return unsub_all
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
"""Test Google report state."""
|
"""Test Google report state."""
|
||||||
|
from datetime import timedelta
|
||||||
from unittest.mock import AsyncMock, patch
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
from homeassistant.components.google_assistant import error, report_state
|
from homeassistant.components.google_assistant import error, report_state
|
||||||
|
@ -41,10 +42,25 @@ async def test_report_state(hass, caplog, legacy_patchable_time):
|
||||||
hass.states.async_set("light.kitchen", "on")
|
hass.states.async_set("light.kitchen", "on")
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert len(mock_report.mock_calls) == 1
|
hass.states.async_set("light.kitchen_2", "on")
|
||||||
assert mock_report.mock_calls[0][1][0] == {
|
await hass.async_block_till_done()
|
||||||
"devices": {"states": {"light.kitchen": {"on": True, "online": True}}}
|
|
||||||
}
|
assert len(mock_report.mock_calls) == 0
|
||||||
|
|
||||||
|
async_fire_time_changed(
|
||||||
|
hass, utcnow() + timedelta(seconds=report_state.REPORT_STATE_WINDOW)
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(mock_report.mock_calls) == 1
|
||||||
|
assert mock_report.mock_calls[0][1][0] == {
|
||||||
|
"devices": {
|
||||||
|
"states": {
|
||||||
|
"light.kitchen": {"on": True, "online": True},
|
||||||
|
"light.kitchen_2": {"on": True, "online": True},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Test that if serialize returns same value, we don't send
|
# Test that if serialize returns same value, we don't send
|
||||||
with patch(
|
with patch(
|
||||||
|
@ -57,6 +73,9 @@ async def test_report_state(hass, caplog, legacy_patchable_time):
|
||||||
|
|
||||||
# Changed, but serialize is same, so filtered out by extra check
|
# Changed, but serialize is same, so filtered out by extra check
|
||||||
hass.states.async_set("light.double_report", "off")
|
hass.states.async_set("light.double_report", "off")
|
||||||
|
async_fire_time_changed(
|
||||||
|
hass, utcnow() + timedelta(seconds=report_state.REPORT_STATE_WINDOW)
|
||||||
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert len(mock_report.mock_calls) == 1
|
assert len(mock_report.mock_calls) == 1
|
||||||
|
@ -69,6 +88,9 @@ async def test_report_state(hass, caplog, legacy_patchable_time):
|
||||||
BASIC_CONFIG, "async_report_state_all", AsyncMock()
|
BASIC_CONFIG, "async_report_state_all", AsyncMock()
|
||||||
) as mock_report:
|
) as mock_report:
|
||||||
hass.states.async_set("switch.ac", "on", {"something": "else"})
|
hass.states.async_set("switch.ac", "on", {"something": "else"})
|
||||||
|
async_fire_time_changed(
|
||||||
|
hass, utcnow() + timedelta(seconds=report_state.REPORT_STATE_WINDOW)
|
||||||
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert len(mock_report.mock_calls) == 0
|
assert len(mock_report.mock_calls) == 0
|
||||||
|
@ -81,9 +103,12 @@ async def test_report_state(hass, caplog, legacy_patchable_time):
|
||||||
side_effect=error.SmartHomeError("mock-error", "mock-msg"),
|
side_effect=error.SmartHomeError("mock-error", "mock-msg"),
|
||||||
):
|
):
|
||||||
hass.states.async_set("light.kitchen", "off")
|
hass.states.async_set("light.kitchen", "off")
|
||||||
|
async_fire_time_changed(
|
||||||
|
hass, utcnow() + timedelta(seconds=report_state.REPORT_STATE_WINDOW)
|
||||||
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert "Not reporting state for light.kitchen: mock-error"
|
assert "Not reporting state for light.kitchen: mock-error" in caplog.text
|
||||||
assert len(mock_report.mock_calls) == 0
|
assert len(mock_report.mock_calls) == 0
|
||||||
|
|
||||||
unsub()
|
unsub()
|
||||||
|
@ -92,6 +117,9 @@ async def test_report_state(hass, caplog, legacy_patchable_time):
|
||||||
BASIC_CONFIG, "async_report_state_all", AsyncMock()
|
BASIC_CONFIG, "async_report_state_all", AsyncMock()
|
||||||
) as mock_report:
|
) as mock_report:
|
||||||
hass.states.async_set("light.kitchen", "on")
|
hass.states.async_set("light.kitchen", "on")
|
||||||
|
async_fire_time_changed(
|
||||||
|
hass, utcnow() + timedelta(seconds=report_state.REPORT_STATE_WINDOW)
|
||||||
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert len(mock_report.mock_calls) == 0
|
assert len(mock_report.mock_calls) == 0
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue