Avoid executor jumps in history stats when no update is needed (#49407)

pull/45434/head
J. Nick Koston 2021-04-19 05:23:10 -10:00 committed by GitHub
parent 6d137d2316
commit b8001b951b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 153 additions and 145 deletions

View File

@ -20,7 +20,7 @@ from homeassistant.core import CoreState, callback
from homeassistant.exceptions import TemplateError
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.helpers.reload import setup_reload_service
from homeassistant.helpers.reload import async_setup_reload_service
import homeassistant.util.dt as dt_util
from . import DOMAIN, PLATFORMS
@ -74,9 +74,9 @@ PLATFORM_SCHEMA = vol.All(
# noinspection PyUnusedLocal
def setup_platform(hass, config, add_entities, discovery_info=None):
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the History Stats sensor."""
setup_reload_service(hass, DOMAIN, PLATFORMS)
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
entity_id = config.get(CONF_ENTITY_ID)
entity_states = config.get(CONF_STATE)
@ -90,7 +90,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
if template is not None:
template.hass = hass
add_entities(
async_add_entities(
[
HistoryStatsSensor(
hass, entity_id, entity_states, start, end, duration, sensor_type, name
@ -108,6 +108,7 @@ class HistoryStatsSensor(SensorEntity):
self, hass, entity_id, entity_states, start, end, duration, sensor_type, name
):
"""Initialize the HistoryStats sensor."""
self.hass = hass
self._entity_id = entity_id
self._entity_states = entity_states
self._duration = duration
@ -186,7 +187,7 @@ class HistoryStatsSensor(SensorEntity):
"""Return the icon to use in the frontend, if any."""
return ICON
def update(self):
async def async_update(self):
"""Get the latest data and updates the states."""
# Get previous values of start and end
p_start, p_end = self._period
@ -218,6 +219,11 @@ class HistoryStatsSensor(SensorEntity):
# Don't compute anything as the value cannot have changed
return
await self.hass.async_add_executor_job(
self._update, start, end, now_timestamp, start_timestamp, end_timestamp
)
def _update(self, start, end, now_timestamp, start_timestamp, end_timestamp):
# Get history between start and end
history_list = history.state_changes_during_period(
self.hass, start, end, str(self._entity_id)
@ -265,7 +271,7 @@ class HistoryStatsSensor(SensorEntity):
# Parse start
if self._start is not None:
try:
start_rendered = self._start.render()
start_rendered = self._start.async_render()
except (TemplateError, TypeError) as ex:
HistoryStatsHelper.handle_template_exception(ex, "start")
return
@ -285,7 +291,7 @@ class HistoryStatsSensor(SensorEntity):
# Parse end
if self._end is not None:
try:
end_rendered = self._end.render()
end_rendered = self._end.async_render()
except (TemplateError, TypeError) as ex:
HistoryStatsHelper.handle_template_exception(ex, "end")
return

View File

@ -118,144 +118,6 @@ class TestHistoryStatsSensor(unittest.TestCase):
assert sensor2_end.minute == 0
assert sensor2_end.second == 0
def test_measure(self):
"""Test the history statistics sensor measure."""
t0 = dt_util.utcnow() - timedelta(minutes=40)
t1 = t0 + timedelta(minutes=20)
t2 = dt_util.utcnow() - timedelta(minutes=10)
# Start t0 t1 t2 End
# |--20min--|--20min--|--10min--|--10min--|
# |---off---|---on----|---off---|---on----|
fake_states = {
"binary_sensor.test_id": [
ha.State("binary_sensor.test_id", "on", last_changed=t0),
ha.State("binary_sensor.test_id", "off", last_changed=t1),
ha.State("binary_sensor.test_id", "on", last_changed=t2),
]
}
start = Template("{{ as_timestamp(now()) - 3600 }}", self.hass)
end = Template("{{ now() }}", self.hass)
sensor1 = HistoryStatsSensor(
self.hass, "binary_sensor.test_id", "on", start, end, None, "time", "Test"
)
sensor2 = HistoryStatsSensor(
self.hass, "unknown.id", "on", start, end, None, "time", "Test"
)
sensor3 = HistoryStatsSensor(
self.hass, "binary_sensor.test_id", "on", start, end, None, "count", "test"
)
sensor4 = HistoryStatsSensor(
self.hass, "binary_sensor.test_id", "on", start, end, None, "ratio", "test"
)
assert sensor1._type == "time"
assert sensor3._type == "count"
assert sensor4._type == "ratio"
with patch(
"homeassistant.components.history.state_changes_during_period",
return_value=fake_states,
), patch("homeassistant.components.history.get_state", return_value=None):
sensor1.update()
sensor2.update()
sensor3.update()
sensor4.update()
assert sensor1.state == 0.5
assert sensor2.state is None
assert sensor3.state == 2
assert sensor4.state == 50
def test_measure_multiple(self):
"""Test the history statistics sensor measure for multiple states."""
t0 = dt_util.utcnow() - timedelta(minutes=40)
t1 = t0 + timedelta(minutes=20)
t2 = dt_util.utcnow() - timedelta(minutes=10)
# Start t0 t1 t2 End
# |--20min--|--20min--|--10min--|--10min--|
# |---------|--orange-|-default-|---blue--|
fake_states = {
"input_select.test_id": [
ha.State("input_select.test_id", "orange", last_changed=t0),
ha.State("input_select.test_id", "default", last_changed=t1),
ha.State("input_select.test_id", "blue", last_changed=t2),
]
}
start = Template("{{ as_timestamp(now()) - 3600 }}", self.hass)
end = Template("{{ now() }}", self.hass)
sensor1 = HistoryStatsSensor(
self.hass,
"input_select.test_id",
["orange", "blue"],
start,
end,
None,
"time",
"Test",
)
sensor2 = HistoryStatsSensor(
self.hass,
"unknown.id",
["orange", "blue"],
start,
end,
None,
"time",
"Test",
)
sensor3 = HistoryStatsSensor(
self.hass,
"input_select.test_id",
["orange", "blue"],
start,
end,
None,
"count",
"test",
)
sensor4 = HistoryStatsSensor(
self.hass,
"input_select.test_id",
["orange", "blue"],
start,
end,
None,
"ratio",
"test",
)
assert sensor1._type == "time"
assert sensor3._type == "count"
assert sensor4._type == "ratio"
with patch(
"homeassistant.components.history.state_changes_during_period",
return_value=fake_states,
), patch("homeassistant.components.history.get_state", return_value=None):
sensor1.update()
sensor2.update()
sensor3.update()
sensor4.update()
assert sensor1.state == 0.5
assert sensor2.state is None
assert sensor3.state == 2
assert sensor4.state == 50
def test_wrong_date(self):
"""Test when start or end value is not a timestamp or a date."""
good = Template("{{ now() }}", self.hass)
@ -415,5 +277,145 @@ async def test_reload(hass):
assert hass.states.get("sensor.second_test")
async def test_measure_multiple(hass):
"""Test the history statistics sensor measure for multiple states."""
t0 = dt_util.utcnow() - timedelta(minutes=40)
t1 = t0 + timedelta(minutes=20)
t2 = dt_util.utcnow() - timedelta(minutes=10)
# Start t0 t1 t2 End
# |--20min--|--20min--|--10min--|--10min--|
# |---------|--orange-|-default-|---blue--|
fake_states = {
"input_select.test_id": [
ha.State("input_select.test_id", "orange", last_changed=t0),
ha.State("input_select.test_id", "default", last_changed=t1),
ha.State("input_select.test_id", "blue", last_changed=t2),
]
}
start = Template("{{ as_timestamp(now()) - 3600 }}", hass)
end = Template("{{ now() }}", hass)
sensor1 = HistoryStatsSensor(
hass,
"input_select.test_id",
["orange", "blue"],
start,
end,
None,
"time",
"Test",
)
sensor2 = HistoryStatsSensor(
hass,
"unknown.id",
["orange", "blue"],
start,
end,
None,
"time",
"Test",
)
sensor3 = HistoryStatsSensor(
hass,
"input_select.test_id",
["orange", "blue"],
start,
end,
None,
"count",
"test",
)
sensor4 = HistoryStatsSensor(
hass,
"input_select.test_id",
["orange", "blue"],
start,
end,
None,
"ratio",
"test",
)
assert sensor1._type == "time"
assert sensor3._type == "count"
assert sensor4._type == "ratio"
with patch(
"homeassistant.components.history.state_changes_during_period",
return_value=fake_states,
), patch("homeassistant.components.history.get_state", return_value=None):
await sensor1.async_update()
await sensor2.async_update()
await sensor3.async_update()
await sensor4.async_update()
assert sensor1.state == 0.5
assert sensor2.state is None
assert sensor3.state == 2
assert sensor4.state == 50
async def async_test_measure(hass):
"""Test the history statistics sensor measure."""
t0 = dt_util.utcnow() - timedelta(minutes=40)
t1 = t0 + timedelta(minutes=20)
t2 = dt_util.utcnow() - timedelta(minutes=10)
# Start t0 t1 t2 End
# |--20min--|--20min--|--10min--|--10min--|
# |---off---|---on----|---off---|---on----|
fake_states = {
"binary_sensor.test_id": [
ha.State("binary_sensor.test_id", "on", last_changed=t0),
ha.State("binary_sensor.test_id", "off", last_changed=t1),
ha.State("binary_sensor.test_id", "on", last_changed=t2),
]
}
start = Template("{{ as_timestamp(now()) - 3600 }}", hass)
end = Template("{{ now() }}", hass)
sensor1 = HistoryStatsSensor(
hass, "binary_sensor.test_id", "on", start, end, None, "time", "Test"
)
sensor2 = HistoryStatsSensor(
hass, "unknown.id", "on", start, end, None, "time", "Test"
)
sensor3 = HistoryStatsSensor(
hass, "binary_sensor.test_id", "on", start, end, None, "count", "test"
)
sensor4 = HistoryStatsSensor(
hass, "binary_sensor.test_id", "on", start, end, None, "ratio", "test"
)
assert sensor1._type == "time"
assert sensor3._type == "count"
assert sensor4._type == "ratio"
with patch(
"homeassistant.components.history.state_changes_during_period",
return_value=fake_states,
), patch("homeassistant.components.history.get_state", return_value=None):
await sensor1.async_update()
await sensor2.async_update()
await sensor3.async_update()
await sensor4.async_update()
assert sensor1.state == 0.5
assert sensor2.state is None
assert sensor3.state == 2
assert sensor4.state == 50
def _get_fixtures_base_path():
return path.dirname(path.dirname(path.dirname(__file__)))