|
2 | 2 |
|
3 | 3 | from __future__ import annotations |
4 | 4 |
|
| 5 | +from asyncio import Event as AsyncioEvent |
5 | 6 | from collections.abc import Sequence |
6 | 7 | from datetime import datetime, timedelta |
7 | 8 | import statistics |
| 9 | +from threading import Event |
8 | 10 | from typing import Any |
9 | 11 | from unittest.mock import patch |
10 | 12 |
|
11 | 13 | from freezegun import freeze_time |
12 | 14 | import pytest |
13 | 15 |
|
14 | 16 | from homeassistant import config as hass_config |
15 | | -from homeassistant.components.recorder import Recorder |
| 17 | +from homeassistant.components.recorder import Recorder, history |
16 | 18 | from homeassistant.components.sensor import ( |
17 | 19 | ATTR_STATE_CLASS, |
18 | 20 | SensorDeviceClass, |
|
50 | 52 |
|
51 | 53 | VALUES_BINARY = ["on", "off", "on", "off", "on", "off", "on", "off", "on"] |
52 | 54 | VALUES_NUMERIC = [17, 20, 15.2, 5, 3.8, 9.2, 6.7, 14, 6] |
| 55 | +VALUES_NUMERIC_LINEAR = [1, 2, 3, 4, 5, 6, 7, 8, 9] |
53 | 56 |
|
54 | 57 |
|
55 | 58 | async def test_unique_id( |
@@ -1701,3 +1704,76 @@ async def test_device_id( |
1701 | 1704 | statistics_entity = entity_registry.async_get("sensor.statistics") |
1702 | 1705 | assert statistics_entity is not None |
1703 | 1706 | assert statistics_entity.device_id == source_entity.device_id |
| 1707 | + |
| 1708 | + |
| 1709 | +async def test_update_before_load(recorder_mock: Recorder, hass: HomeAssistant) -> None: |
| 1710 | + """Verify that updates happening before reloading from the database are handled correctly.""" |
| 1711 | + |
| 1712 | + current_time = dt_util.utcnow() |
| 1713 | + |
| 1714 | + # enable and pre-fill the recorder |
| 1715 | + await hass.async_block_till_done() |
| 1716 | + await async_wait_recording_done(hass) |
| 1717 | + |
| 1718 | + with ( |
| 1719 | + freeze_time(current_time) as freezer, |
| 1720 | + ): |
| 1721 | + for value in VALUES_NUMERIC_LINEAR: |
| 1722 | + hass.states.async_set( |
| 1723 | + "sensor.test_monitored", |
| 1724 | + str(value), |
| 1725 | + {ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS}, |
| 1726 | + ) |
| 1727 | + await hass.async_block_till_done() |
| 1728 | + current_time += timedelta(seconds=1) |
| 1729 | + freezer.move_to(current_time) |
| 1730 | + |
| 1731 | + await async_wait_recording_done(hass) |
| 1732 | + |
| 1733 | + # some synchronisation is needed to prevent that loading from the database finishes too soon |
| 1734 | + # we want this to take long enough to be able to try to add a value BEFORE loading is done |
| 1735 | + state_changes_during_period_called_evt = AsyncioEvent() |
| 1736 | + state_changes_during_period_stall_evt = Event() |
| 1737 | + real_state_changes_during_period = history.state_changes_during_period |
| 1738 | + |
| 1739 | + def mock_state_changes_during_period(*args, **kwargs): |
| 1740 | + states = real_state_changes_during_period(*args, **kwargs) |
| 1741 | + hass.loop.call_soon_threadsafe(state_changes_during_period_called_evt.set) |
| 1742 | + state_changes_during_period_stall_evt.wait() |
| 1743 | + return states |
| 1744 | + |
| 1745 | + # create the statistics component, get filled from database |
| 1746 | + with patch( |
| 1747 | + "homeassistant.components.statistics.sensor.history.state_changes_during_period", |
| 1748 | + mock_state_changes_during_period, |
| 1749 | + ): |
| 1750 | + assert await async_setup_component( |
| 1751 | + hass, |
| 1752 | + "sensor", |
| 1753 | + { |
| 1754 | + "sensor": [ |
| 1755 | + { |
| 1756 | + "platform": "statistics", |
| 1757 | + "name": "test", |
| 1758 | + "entity_id": "sensor.test_monitored", |
| 1759 | + "state_characteristic": "average_step", |
| 1760 | + "max_age": {"seconds": 10}, |
| 1761 | + }, |
| 1762 | + ] |
| 1763 | + }, |
| 1764 | + ) |
| 1765 | + # adding this value is going to be ignored, since loading from the database hasn't finished yet |
| 1766 | + # if this value would be added before loading from the database is done |
| 1767 | + # it would mess up the order of the internal queue which is supposed to be sorted by time |
| 1768 | + await state_changes_during_period_called_evt.wait() |
| 1769 | + hass.states.async_set( |
| 1770 | + "sensor.test_monitored", |
| 1771 | + "10", |
| 1772 | + {ATTR_UNIT_OF_MEASUREMENT: DEGREE}, |
| 1773 | + ) |
| 1774 | + state_changes_during_period_stall_evt.set() |
| 1775 | + await hass.async_block_till_done() |
| 1776 | + |
| 1777 | + # we will end up with a buffer of [1 .. 9] (10 wasn't added) |
| 1778 | + # so the computed average_step is 1+2+3+4+5+6+7+8/8 = 4.5 |
| 1779 | + assert float(hass.states.get("sensor.test").state) == pytest.approx(4.5) |
0 commit comments