diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 656e9fb00f0..7c04b61edca 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -6,7 +6,6 @@ from collections.abc import Iterable, MutableMapping import datetime import itertools import logging -import math from typing import Any from sqlalchemy.orm.session import Session @@ -37,6 +36,7 @@ from homeassistant.util import dt as dt_util from . import ( ATTR_LAST_RESET, + ATTR_NUMERICAL_VALUE, ATTR_OPTIONS, ATTR_STATE_CLASS, DOMAIN, @@ -142,14 +142,6 @@ def _equivalent_units(units: set[str | None]) -> bool: return len(units) == 1 -def _parse_float(state: str) -> float: - """Parse a float string, throw on inf or nan.""" - fstate = float(state) - if math.isnan(fstate) or math.isinf(fstate): - raise ValueError - return fstate - - def _normalize_states( hass: HomeAssistant, session: Session, @@ -163,9 +155,7 @@ def _normalize_states( fstates: list[tuple[float, State]] = [] for state in entity_history: - try: - fstate = _parse_float(state.state) - except (ValueError, TypeError): # TypeError to guard for NULL state in DB + if (fstate := state.attributes.get(ATTR_NUMERICAL_VALUE)) is None: continue fstates.append((fstate, state)) @@ -298,7 +288,7 @@ def warn_dip( ), entity_id, f"from integration {domain} " if domain else "", - state.state, + state.attributes[ATTR_NUMERICAL_VALUE], previous_fstate, state.last_updated.isoformat(), _suggest_report_issue(hass, entity_id), @@ -319,7 +309,7 @@ def warn_negative(hass: HomeAssistant, entity_id: str, state: State) -> None: ), entity_id, f"from integration {domain} " if domain else "", - state.state, + state.attributes[ATTR_NUMERICAL_VALUE], state.last_updated.isoformat(), _suggest_report_issue(hass, entity_id), ) @@ -424,6 +414,7 @@ def _compile_statistics( # noqa: C901 start - datetime.timedelta.resolution, end, entity_ids=entities_significant_history, + significant_changes_only=False, ) history_list = {**history_list, **_history_list} # If there are no recent state changes, the sensor's state may already be pruned diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 635b3f0fde6..51b7afefa06 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -55,7 +55,9 @@ def test_compile_hourly_statistics(hass_recorder): instance = recorder.get_instance(hass) setup_component(hass, "sensor", {}) zero, four, states = record_states(hass) - hist = history.get_significant_states(hass, zero, four) + hist = history.get_significant_states( + hass, zero, four, significant_changes_only=False + ) assert_dict_of_states_equal_without_context_and_last_changed(states, hist) # Should not fail if there is nothing there yet @@ -316,7 +318,9 @@ def test_rename_entity(hass_recorder): hass.block_till_done() zero, four, states = record_states(hass) - hist = history.get_significant_states(hass, zero, four) + hist = history.get_significant_states( + hass, zero, four, significant_changes_only=False + ) assert_dict_of_states_equal_without_context_and_last_changed(states, hist) for kwargs in ({}, {"statistic_ids": ["sensor.test1"]}): @@ -382,7 +386,9 @@ def test_rename_entity_collision(hass_recorder, caplog): hass.block_till_done() zero, four, states = record_states(hass) - hist = history.get_significant_states(hass, zero, four) + hist = history.get_significant_states( + hass, zero, four, significant_changes_only=False + ) assert_dict_of_states_equal_without_context_and_last_changed(states, hist) for kwargs in ({}, {"statistic_ids": ["sensor.test1"]}): @@ -447,7 +453,9 @@ def test_statistics_duplicated(hass_recorder, caplog): hass = hass_recorder() setup_component(hass, "sensor", {}) zero, four, states = record_states(hass) - hist = history.get_significant_states(hass, zero, four) + hist = history.get_significant_states( + hass, zero, four, significant_changes_only=False + ) assert_dict_of_states_equal_without_context_and_last_changed(states, hist) wait_recording_done(hass) @@ -1709,6 +1717,14 @@ def record_states(hass): wait_recording_done(hass) return hass.states.get(entity_id) + def set_sensor_state(entity_id, numerical_value, attributes): + """Set the state.""" + hass.states.set( + entity_id, "", attributes={**attributes, "numerical_value": numerical_value} + ) + wait_recording_done(hass) + return hass.states.get(entity_id) + zero = dt_util.utcnow() one = zero + timedelta(seconds=1 * 5) two = one + timedelta(seconds=15 * 5) @@ -1725,25 +1741,25 @@ def record_states(hass): states[mp].append( set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt2)}) ) - states[sns1].append(set_state(sns1, "10", attributes=sns1_attr)) - states[sns2].append(set_state(sns2, "10", attributes=sns2_attr)) - states[sns3].append(set_state(sns3, "10", attributes=sns3_attr)) - states[sns4].append(set_state(sns4, "10", attributes=sns4_attr)) + states[sns1].append(set_sensor_state(sns1, 10, attributes=sns1_attr)) + states[sns2].append(set_sensor_state(sns2, 10, attributes=sns2_attr)) + states[sns3].append(set_sensor_state(sns3, 10, attributes=sns3_attr)) + states[sns4].append(set_sensor_state(sns4, 10, attributes=sns4_attr)) with patch( "homeassistant.components.recorder.core.dt_util.utcnow", return_value=two ): - states[sns1].append(set_state(sns1, "15", attributes=sns1_attr)) - states[sns2].append(set_state(sns2, "15", attributes=sns2_attr)) - states[sns3].append(set_state(sns3, "15", attributes=sns3_attr)) - states[sns4].append(set_state(sns4, "15", attributes=sns4_attr)) + states[sns1].append(set_sensor_state(sns1, 15, attributes=sns1_attr)) + states[sns2].append(set_sensor_state(sns2, 15, attributes=sns2_attr)) + states[sns3].append(set_sensor_state(sns3, 15, attributes=sns3_attr)) + states[sns4].append(set_sensor_state(sns4, 15, attributes=sns4_attr)) with patch( "homeassistant.components.recorder.core.dt_util.utcnow", return_value=three ): - states[sns1].append(set_state(sns1, "20", attributes=sns1_attr)) - states[sns2].append(set_state(sns2, "20", attributes=sns2_attr)) - states[sns3].append(set_state(sns3, "20", attributes=sns3_attr)) - states[sns4].append(set_state(sns4, "20", attributes=sns4_attr)) + states[sns1].append(set_sensor_state(sns1, 20, attributes=sns1_attr)) + states[sns2].append(set_sensor_state(sns2, 20, attributes=sns2_attr)) + states[sns3].append(set_sensor_state(sns3, 20, attributes=sns3_attr)) + states[sns4].append(set_sensor_state(sns4, 20, attributes=sns4_attr)) return zero, four, states diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 6c0e2f8dfd6..331f25331ce 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -17,6 +17,7 @@ from homeassistant.components.recorder.statistics import ( get_metadata, list_statistic_ids, ) +from homeassistant.components.sensor.const import ATTR_NUMERICAL_VALUE from homeassistant.helpers import recorder as recorder_helper from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -131,7 +132,11 @@ async def test_statistics_during_period(recorder_mock, hass, hass_ws_client): hass.config.units = US_CUSTOMARY_SYSTEM await async_setup_component(hass, "sensor", {}) await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.test", 10, attributes=POWER_SENSOR_KW_ATTRIBUTES) + hass.states.async_set( + "sensor.test", + "", + attributes=POWER_SENSOR_KW_ATTRIBUTES | {ATTR_NUMERICAL_VALUE: 10}, + ) await async_wait_recording_done(hass) do_adhoc_statistics(hass, start=now) @@ -892,7 +897,9 @@ async def test_statistics_during_period_unit_conversion( await async_setup_component(hass, "sensor", {}) await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.test", state, attributes=attributes) + hass.states.async_set( + "sensor.test", "", attributes=attributes | {ATTR_NUMERICAL_VALUE: state} + ) await async_wait_recording_done(hass) do_adhoc_statistics(hass, start=now) @@ -983,8 +990,12 @@ async def test_sum_statistics_during_period_unit_conversion( await async_setup_component(hass, "sensor", {}) await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.test", 0, attributes=attributes) - hass.states.async_set("sensor.test", state, attributes=attributes) + hass.states.async_set( + "sensor.test", "", attributes=attributes | {ATTR_NUMERICAL_VALUE: 0} + ) + hass.states.async_set( + "sensor.test", "", attributes=attributes | {ATTR_NUMERICAL_VALUE: state} + ) await async_wait_recording_done(hass) do_adhoc_statistics(hass, start=now) @@ -1112,7 +1123,11 @@ async def test_statistics_during_period_in_the_past( past = now - timedelta(days=3) with freeze_time(past): - hass.states.async_set("sensor.test", 10, attributes=POWER_SENSOR_KW_ATTRIBUTES) + hass.states.async_set( + "sensor.test", + "", + attributes=POWER_SENSOR_KW_ATTRIBUTES | {ATTR_NUMERICAL_VALUE: 10}, + ) await async_wait_recording_done(hass) sensor_state = hass.states.get("sensor.test") @@ -1340,7 +1355,9 @@ async def test_list_statistic_ids( assert response["success"] assert response["result"] == [] - hass.states.async_set("sensor.test", 10, attributes=attributes) + hass.states.async_set( + "sensor.test", "", attributes=attributes | {ATTR_NUMERICAL_VALUE: 10} + ) await async_wait_recording_done(hass) await client.send_json({"id": 2, "type": "recorder/list_statistic_ids"}) @@ -1503,7 +1520,9 @@ async def test_list_statistic_ids_unit_change( assert response["success"] assert response["result"] == [] - hass.states.async_set("sensor.test", 10, attributes=attributes) + hass.states.async_set( + "sensor.test", "", attributes=attributes | {ATTR_NUMERICAL_VALUE: 10} + ) await async_wait_recording_done(hass) do_adhoc_statistics(hass, start=now) @@ -1526,7 +1545,9 @@ async def test_list_statistic_ids_unit_change( ] # Change the state unit - hass.states.async_set("sensor.test", 10, attributes=attributes2) + hass.states.async_set( + "sensor.test", "", attributes=attributes2 | {ATTR_NUMERICAL_VALUE: 10} + ) await client.send_json({"id": 3, "type": "recorder/list_statistic_ids"}) response = await client.receive_json() @@ -1579,9 +1600,15 @@ async def test_clear_statistics(recorder_mock, hass, hass_ws_client): hass.config.units = units await async_setup_component(hass, "sensor", {}) await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.test1", state, attributes=attributes) - hass.states.async_set("sensor.test2", state * 2, attributes=attributes) - hass.states.async_set("sensor.test3", state * 3, attributes=attributes) + hass.states.async_set( + "sensor.test1", "", attributes=attributes | {ATTR_NUMERICAL_VALUE: state} + ) + hass.states.async_set( + "sensor.test2", "", attributes=attributes | {ATTR_NUMERICAL_VALUE: state * 2} + ) + hass.states.async_set( + "sensor.test3", "", attributes=attributes | {ATTR_NUMERICAL_VALUE: state * 3} + ) await async_wait_recording_done(hass) do_adhoc_statistics(hass, start=now) @@ -1704,7 +1731,9 @@ async def test_update_statistics_metadata( hass.config.units = units await async_setup_component(hass, "sensor", {}) await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.test", state, attributes=attributes) + hass.states.async_set( + "sensor.test", "", attributes=attributes | {ATTR_NUMERICAL_VALUE: state} + ) await async_wait_recording_done(hass) do_adhoc_statistics(hass, period="hourly", start=now) @@ -1795,7 +1824,9 @@ async def test_change_statistics_unit(recorder_mock, hass, hass_ws_client): hass.config.units = units await async_setup_component(hass, "sensor", {}) await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.test", state, attributes=attributes) + hass.states.async_set( + "sensor.test", "", attributes=attributes | {ATTR_NUMERICAL_VALUE: state} + ) await async_wait_recording_done(hass) do_adhoc_statistics(hass, period="hourly", start=now) @@ -1968,7 +1999,9 @@ async def test_change_statistics_unit_errors( hass.config.units = units await async_setup_component(hass, "sensor", {}) await async_recorder_block_till_done(hass) - hass.states.async_set("sensor.test", state, attributes=attributes) + hass.states.async_set( + "sensor.test", "", attributes=attributes | {ATTR_NUMERICAL_VALUE: state} + ) await async_wait_recording_done(hass) do_adhoc_statistics(hass, period="hourly", start=now) @@ -2311,10 +2344,14 @@ async def test_get_statistics_metadata( } ] - hass.states.async_set("sensor.test", 10, attributes=attributes) + hass.states.async_set( + "sensor.test", "", attributes=attributes | {ATTR_NUMERICAL_VALUE: 10} + ) await async_wait_recording_done(hass) - hass.states.async_set("sensor.test2", 10, attributes=attributes) + hass.states.async_set( + "sensor.test2", "", attributes=attributes | {ATTR_NUMERICAL_VALUE: 10} + ) await async_wait_recording_done(hass) await client.send_json( diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index e3df80d54df..129178bc26b 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -25,7 +25,11 @@ from homeassistant.components.recorder.statistics import ( list_statistic_ids, ) from homeassistant.components.recorder.util import get_instance, session_scope -from homeassistant.components.sensor import ATTR_OPTIONS, DOMAIN +from homeassistant.components.sensor.const import ( + ATTR_NUMERICAL_VALUE, + ATTR_OPTIONS, + DOMAIN, +) from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant, State from homeassistant.setup import async_setup_component, setup_component @@ -140,7 +144,9 @@ def test_compile_hourly_statistics( "unit_of_measurement": state_unit, } four, states = record_states(hass, zero, "sensor.test1", attributes) - hist = history.get_significant_states(hass, zero, four) + hist = history.get_significant_states( + hass, zero, four, significant_changes_only=False + ) assert_dict_of_states_equal_without_context_and_last_changed(states, hist) do_adhoc_statistics(hass, start=zero) @@ -202,10 +208,12 @@ def test_compile_hourly_statistics_purged_state_changes( "unit_of_measurement": state_unit, } four, states = record_states(hass, zero, "sensor.test1", attributes) - hist = history.get_significant_states(hass, zero, four) + hist = history.get_significant_states( + hass, zero, four, significant_changes_only=False + ) assert_dict_of_states_equal_without_context_and_last_changed(states, hist) - mean = min = max = float(hist["sensor.test1"][-1].state) + mean = min = max = hist["sensor.test1"][-1].attributes[ATTR_NUMERICAL_VALUE] # Purge all states from the database with patch( @@ -214,7 +222,9 @@ def test_compile_hourly_statistics_purged_state_changes( hass.services.call("recorder", "purge", {"keep_days": 0}) hass.block_till_done() wait_recording_done(hass) - hist = history.get_significant_states(hass, zero, four) + hist = history.get_significant_states( + hass, zero, four, significant_changes_only=False + ) assert not hist do_adhoc_statistics(hass, start=zero) @@ -283,7 +293,9 @@ def test_compile_hourly_statistics_wrong_unit(hass_recorder, caplog, attributes) _, _states = record_states(hass, zero, "sensor.test7", attributes_tmp) states = {**states, **_states} - hist = history.get_significant_states(hass, zero, four) + hist = history.get_significant_states( + hass, zero, four, significant_changes_only=False + ) assert_dict_of_states_equal_without_context_and_last_changed(states, hist) do_adhoc_statistics(hass, start=zero) @@ -474,7 +486,10 @@ async def test_compile_hourly_sum_statistics_amount( ) await async_wait_recording_done(hass) hist = history.get_significant_states( - hass, period0 - timedelta.resolution, eight + timedelta.resolution + hass, + period0 - timedelta.resolution, + eight + timedelta.resolution, + significant_changes_only=False, ) assert_multiple_states_equal_without_context_and_last_changed( dict(states)["sensor.test1"], dict(hist)["sensor.test1"] @@ -857,6 +872,11 @@ def test_compile_hourly_sum_statistics_nan_inf_state( one + timedelta.resolution, significant_changes_only=False, ) + # Recorder will record state attributes with nan and inf replaced with null + for state in states["sensor.test1"]: + if not math.isfinite(state.attributes[ATTR_NUMERICAL_VALUE]): + state.attributes = state.attributes | {ATTR_NUMERICAL_VALUE: None} + assert_multiple_states_equal_without_context_and_last_changed( dict(states)["sensor.test1"], dict(hist)["sensor.test1"] ) @@ -1019,7 +1039,7 @@ def test_compile_hourly_sum_statistics_negative_state( }, ] assert "Error while processing event StatisticsTask" not in caplog.text - state = states[entity_id][offending_state].state + state = states[entity_id][offending_state].attributes[ATTR_NUMERICAL_VALUE] last_updated = states[entity_id][offending_state].last_updated.isoformat() assert ( f"Entity {entity_id} {warning_1}has state class total_increasing, but its state " @@ -1070,7 +1090,10 @@ def test_compile_hourly_sum_statistics_total_no_reset( ) wait_recording_done(hass) hist = history.get_significant_states( - hass, period0 - timedelta.resolution, eight + timedelta.resolution + hass, + period0 - timedelta.resolution, + eight + timedelta.resolution, + significant_changes_only=False, ) assert_multiple_states_equal_without_context_and_last_changed( dict(states)["sensor.test1"], dict(hist)["sensor.test1"] @@ -1172,7 +1195,10 @@ def test_compile_hourly_sum_statistics_total_increasing( ) wait_recording_done(hass) hist = history.get_significant_states( - hass, period0 - timedelta.resolution, eight + timedelta.resolution + hass, + period0 - timedelta.resolution, + eight + timedelta.resolution, + significant_changes_only=False, ) assert_multiple_states_equal_without_context_and_last_changed( dict(states)["sensor.test1"], dict(hist)["sensor.test1"] @@ -1272,7 +1298,10 @@ def test_compile_hourly_sum_statistics_total_increasing_small_dip( ) wait_recording_done(hass) hist = history.get_significant_states( - hass, period0 - timedelta.resolution, eight + timedelta.resolution + hass, + period0 - timedelta.resolution, + eight + timedelta.resolution, + significant_changes_only=False, ) assert_multiple_states_equal_without_context_and_last_changed( dict(states)["sensor.test1"], dict(hist)["sensor.test1"] @@ -1288,8 +1317,8 @@ def test_compile_hourly_sum_statistics_total_increasing_small_dip( ) not in caplog.text do_adhoc_statistics(hass, start=period2) wait_recording_done(hass) - state = states["sensor.test1"][6].state - previous_state = float(states["sensor.test1"][5].state) + state = states["sensor.test1"][6].attributes[ATTR_NUMERICAL_VALUE] + previous_state = float(states["sensor.test1"][5].attributes[ATTR_NUMERICAL_VALUE]) last_updated = states["sensor.test1"][6].last_updated.isoformat() assert ( "Entity sensor.test1 has state class total_increasing, but its state is not " @@ -1379,7 +1408,10 @@ def test_compile_hourly_energy_statistics_unsupported(hass_recorder, caplog): wait_recording_done(hass) hist = history.get_significant_states( - hass, period0 - timedelta.resolution, eight + timedelta.resolution + hass, + period0 - timedelta.resolution, + eight + timedelta.resolution, + significant_changes_only=False, ) assert_multiple_states_equal_without_context_and_last_changed( dict(states)["sensor.test1"], dict(hist)["sensor.test1"] @@ -1471,7 +1503,10 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): states = {**states, **_states} wait_recording_done(hass) hist = history.get_significant_states( - hass, period0 - timedelta.resolution, eight + timedelta.resolution + hass, + period0 - timedelta.resolution, + eight + timedelta.resolution, + significant_changes_only=False, ) assert_multiple_states_equal_without_context_and_last_changed( dict(states)["sensor.test1"], dict(hist)["sensor.test1"] @@ -1656,7 +1691,9 @@ def test_compile_hourly_statistics_unchanged( "unit_of_measurement": state_unit, } four, states = record_states(hass, zero, "sensor.test1", attributes) - hist = history.get_significant_states(hass, zero, four) + hist = history.get_significant_states( + hass, zero, four, significant_changes_only=False + ) assert_dict_of_states_equal_without_context_and_last_changed(states, hist) do_adhoc_statistics(hass, start=four) @@ -1688,7 +1725,9 @@ def test_compile_hourly_statistics_partially_unavailable(hass_recorder, caplog): four, states = record_states_partially_unavailable( hass, zero, "sensor.test1", TEMPERATURE_SENSOR_ATTRIBUTES ) - hist = history.get_significant_states(hass, zero, four) + hist = history.get_significant_states( + hass, zero, four, significant_changes_only=False + ) assert_dict_of_states_equal_without_context_and_last_changed(states, hist) do_adhoc_statistics(hass, start=zero) @@ -1757,7 +1796,9 @@ def test_compile_hourly_statistics_unavailable( ) _, _states = record_states(hass, zero, "sensor.test2", attributes) states = {**states, **_states} - hist = history.get_significant_states(hass, zero, four) + hist = history.get_significant_states( + hass, zero, four, significant_changes_only=False + ) assert_dict_of_states_equal_without_context_and_last_changed(states, hist) do_adhoc_statistics(hass, start=four) @@ -1965,7 +2006,9 @@ def test_compile_hourly_statistics_changing_units_1( hass, zero + timedelta(minutes=10), "sensor.test1", attributes ) states["sensor.test1"] += _states["sensor.test1"] - hist = history.get_significant_states(hass, zero, four) + hist = history.get_significant_states( + hass, zero, four, significant_changes_only=False + ) assert_dict_of_states_equal_without_context_and_last_changed(states, hist) do_adhoc_statistics(hass, start=zero) @@ -2075,7 +2118,9 @@ def test_compile_hourly_statistics_changing_units_2( hass, zero + timedelta(minutes=5), "sensor.test1", attributes ) states["sensor.test1"] += _states["sensor.test1"] - hist = history.get_significant_states(hass, zero, four) + hist = history.get_significant_states( + hass, zero, four, significant_changes_only=False + ) assert_dict_of_states_equal_without_context_and_last_changed(states, hist) do_adhoc_statistics(hass, start=zero + timedelta(seconds=30 * 5)) @@ -2143,7 +2188,9 @@ def test_compile_hourly_statistics_changing_units_3( hass, zero + timedelta(minutes=10), "sensor.test1", attributes ) states["sensor.test1"] += _states["sensor.test1"] - hist = history.get_significant_states(hass, zero, four) + hist = history.get_significant_states( + hass, zero, four, significant_changes_only=False + ) assert_dict_of_states_equal_without_context_and_last_changed(states, hist) do_adhoc_statistics(hass, start=zero) @@ -2291,7 +2338,9 @@ def test_compile_hourly_statistics_convert_units_1( hass, zero + timedelta(minutes=10), "sensor.test1", attributes ) states["sensor.test1"] += _states["sensor.test1"] - hist = history.get_significant_states(hass, zero, four) + hist = history.get_significant_states( + hass, zero, four, significant_changes_only=False + ) assert_dict_of_states_equal_without_context_and_last_changed(states, hist) do_adhoc_statistics(hass, start=zero + timedelta(minutes=10)) wait_recording_done(hass) @@ -2383,7 +2432,9 @@ def test_compile_hourly_statistics_equivalent_units_1( hass, zero + timedelta(minutes=10), "sensor.test1", attributes ) states["sensor.test1"] += _states["sensor.test1"] - hist = history.get_significant_states(hass, zero, four) + hist = history.get_significant_states( + hass, zero, four, significant_changes_only=False + ) assert_dict_of_states_equal_without_context_and_last_changed(states, hist) do_adhoc_statistics(hass, start=zero) @@ -2497,7 +2548,9 @@ def test_compile_hourly_statistics_equivalent_units_2( hass, zero + timedelta(minutes=5), "sensor.test1", attributes ) states["sensor.test1"] += _states["sensor.test1"] - hist = history.get_significant_states(hass, zero, four) + hist = history.get_significant_states( + hass, zero, four, significant_changes_only=False + ) assert_dict_of_states_equal_without_context_and_last_changed(states, hist) do_adhoc_statistics(hass, start=zero + timedelta(seconds=30 * 5)) @@ -2612,7 +2665,9 @@ def test_compile_hourly_statistics_changing_device_class_1( hass, zero + timedelta(minutes=10), "sensor.test1", attributes ) states["sensor.test1"] += _states["sensor.test1"] - hist = history.get_significant_states(hass, zero, four) + hist = history.get_significant_states( + hass, zero, four, significant_changes_only=False + ) assert_dict_of_states_equal_without_context_and_last_changed(states, hist) # Run statistics again, additional statistics is generated @@ -2667,7 +2722,9 @@ def test_compile_hourly_statistics_changing_device_class_1( hass, zero + timedelta(minutes=20), "sensor.test1", attributes ) states["sensor.test1"] += _states["sensor.test1"] - hist = history.get_significant_states(hass, zero, four) + hist = history.get_significant_states( + hass, zero, four, significant_changes_only=False + ) assert_dict_of_states_equal_without_context_and_last_changed(states, hist) # Run statistics again, additional statistics is generated @@ -2802,7 +2859,9 @@ def test_compile_hourly_statistics_changing_device_class_2( hass, zero + timedelta(minutes=10), "sensor.test1", attributes ) states["sensor.test1"] += _states["sensor.test1"] - hist = history.get_significant_states(hass, zero, four) + hist = history.get_significant_states( + hass, zero, four, significant_changes_only=False + ) assert_dict_of_states_equal_without_context_and_last_changed(states, hist) # Run statistics again, additional statistics is generated @@ -2918,7 +2977,9 @@ def test_compile_hourly_statistics_changing_state_class( # Add more states, with changed state class four, _states = record_states(hass, period1, "sensor.test1", attributes_2) states["sensor.test1"] += _states["sensor.test1"] - hist = history.get_significant_states(hass, period0, four) + hist = history.get_significant_states( + hass, period0, four, significant_changes_only=False + ) assert_dict_of_states_equal_without_context_and_last_changed(states, hist) do_adhoc_statistics(hass, start=period1) @@ -3401,9 +3462,11 @@ def record_states(hass, zero, entity_id, attributes, seq=None): if seq is None: seq = [-10, 15, 30] - def set_state(entity_id, state, **kwargs): + def set_state(entity_id, numerical_value, attributes): """Set the state.""" - hass.states.set(entity_id, state, **kwargs) + hass.states.set( + entity_id, "", attributes={**attributes, "numerical_value": numerical_value} + ) wait_recording_done(hass) return hass.states.get(entity_id) @@ -3416,23 +3479,17 @@ def record_states(hass, zero, entity_id, attributes, seq=None): with patch( "homeassistant.components.recorder.core.dt_util.utcnow", return_value=one ): - states[entity_id].append( - set_state(entity_id, str(seq[0]), attributes=attributes) - ) + states[entity_id].append(set_state(entity_id, seq[0], attributes=attributes)) with patch( "homeassistant.components.recorder.core.dt_util.utcnow", return_value=two ): - states[entity_id].append( - set_state(entity_id, str(seq[1]), attributes=attributes) - ) + states[entity_id].append(set_state(entity_id, seq[1], attributes=attributes)) with patch( "homeassistant.components.recorder.core.dt_util.utcnow", return_value=three ): - states[entity_id].append( - set_state(entity_id, str(seq[2]), attributes=attributes) - ) + states[entity_id].append(set_state(entity_id, seq[2], attributes=attributes)) return four, states @@ -3504,14 +3561,19 @@ async def test_validate_unit_change_convertible( # No statistics, unit in state matching device class - empty response hass.states.async_set( - "sensor.test", 10, attributes={**attributes, **{"unit_of_measurement": unit}} + "sensor.test", + "", + attributes=attributes | {"unit_of_measurement": unit, ATTR_NUMERICAL_VALUE: 10}, ) await async_recorder_block_till_done(hass) await assert_validation_result(client, {}) # No statistics, unit in state not matching device class - empty response hass.states.async_set( - "sensor.test", 11, attributes={**attributes, **{"unit_of_measurement": "dogs"}} + "sensor.test", + "", + attributes=attributes + | {"unit_of_measurement": "dogs", ATTR_NUMERICAL_VALUE: 11}, ) await async_recorder_block_till_done(hass) await assert_validation_result(client, {}) @@ -3520,7 +3582,10 @@ async def test_validate_unit_change_convertible( await async_recorder_block_till_done(hass) do_adhoc_statistics(hass, start=now) hass.states.async_set( - "sensor.test", 12, attributes={**attributes, **{"unit_of_measurement": "dogs"}} + "sensor.test", + "", + attributes=attributes + | {"unit_of_measurement": "dogs", ATTR_NUMERICAL_VALUE: 12}, ) await async_recorder_block_till_done(hass) expected = { @@ -3540,7 +3605,9 @@ async def test_validate_unit_change_convertible( # Valid state - empty response hass.states.async_set( - "sensor.test", 13, attributes={**attributes, **{"unit_of_measurement": unit}} + "sensor.test", + "", + attributes=attributes | {"unit_of_measurement": unit, ATTR_NUMERICAL_VALUE: 13}, ) await async_recorder_block_till_done(hass) await assert_validation_result(client, {}) @@ -3552,7 +3619,10 @@ async def test_validate_unit_change_convertible( # Valid state in compatible unit - empty response hass.states.async_set( - "sensor.test", 13, attributes={**attributes, **{"unit_of_measurement": unit2}} + "sensor.test", + "", + attributes=attributes + | {"unit_of_measurement": unit2, ATTR_NUMERICAL_VALUE: 14}, ) await async_recorder_block_till_done(hass) await assert_validation_result(client, {}) @@ -3615,7 +3685,9 @@ async def test_validate_statistics_unit_ignore_device_class( # No statistics, no device class - empty response initial_attributes = {"state_class": "measurement", "unit_of_measurement": "dogs"} - hass.states.async_set("sensor.test", 10, attributes=initial_attributes) + hass.states.async_set( + "sensor.test", "", attributes=initial_attributes | {ATTR_NUMERICAL_VALUE: 10} + ) await hass.async_block_till_done() await assert_validation_result(client, {}) @@ -3623,7 +3695,10 @@ async def test_validate_statistics_unit_ignore_device_class( do_adhoc_statistics(hass, start=now) await async_recorder_block_till_done(hass) hass.states.async_set( - "sensor.test", 12, attributes={**attributes, **{"unit_of_measurement": "dogs"}} + "sensor.test", + "", + attributes=initial_attributes + | {"unit_of_measurement": "dogs", ATTR_NUMERICAL_VALUE: 12}, ) await hass.async_block_till_done() await assert_validation_result(client, {}) @@ -3703,14 +3778,19 @@ async def test_validate_statistics_unit_change_no_device_class( # No statistics, sensor state set - empty response hass.states.async_set( - "sensor.test", 10, attributes={**attributes, **{"unit_of_measurement": unit}} + "sensor.test", + "", + attributes=attributes | {"unit_of_measurement": unit, ATTR_NUMERICAL_VALUE: 10}, ) await async_recorder_block_till_done(hass) await assert_validation_result(client, {}) # No statistics, sensor state set to an incompatible unit - empty response hass.states.async_set( - "sensor.test", 11, attributes={**attributes, **{"unit_of_measurement": "dogs"}} + "sensor.test", + "", + attributes=attributes + | {"unit_of_measurement": "dogs", ATTR_NUMERICAL_VALUE: 11}, ) await async_recorder_block_till_done(hass) await assert_validation_result(client, {}) @@ -3719,7 +3799,10 @@ async def test_validate_statistics_unit_change_no_device_class( await async_recorder_block_till_done(hass) do_adhoc_statistics(hass, start=now) hass.states.async_set( - "sensor.test", 12, attributes={**attributes, **{"unit_of_measurement": "dogs"}} + "sensor.test", + "", + attributes=attributes + | {"unit_of_measurement": "dogs", ATTR_NUMERICAL_VALUE: 12}, ) await async_recorder_block_till_done(hass) expected = { @@ -3739,7 +3822,9 @@ async def test_validate_statistics_unit_change_no_device_class( # Valid state - empty response hass.states.async_set( - "sensor.test", 13, attributes={**attributes, **{"unit_of_measurement": unit}} + "sensor.test", + "", + attributes=attributes | {"unit_of_measurement": unit, ATTR_NUMERICAL_VALUE: 13}, ) await async_recorder_block_till_done(hass) await assert_validation_result(client, {}) @@ -3751,7 +3836,10 @@ async def test_validate_statistics_unit_change_no_device_class( # Valid state in compatible unit - empty response hass.states.async_set( - "sensor.test", 13, attributes={**attributes, **{"unit_of_measurement": unit2}} + "sensor.test", + "", + attributes=attributes + | {"unit_of_measurement": unit2, ATTR_NUMERICAL_VALUE: 14}, ) await async_recorder_block_till_done(hass) await assert_validation_result(client, {}) @@ -3810,7 +3898,9 @@ async def test_validate_statistics_unsupported_state_class( await assert_validation_result(client, {}) # No statistics, valid state - empty response - hass.states.async_set("sensor.test", 10, attributes=attributes) + hass.states.async_set( + "sensor.test", "", attributes=attributes | {ATTR_NUMERICAL_VALUE: 10} + ) await hass.async_block_till_done() await assert_validation_result(client, {}) @@ -3822,7 +3912,9 @@ async def test_validate_statistics_unsupported_state_class( # State update with invalid state class, expect error _attributes = dict(attributes) _attributes.pop("state_class") - hass.states.async_set("sensor.test", 12, attributes=_attributes) + hass.states.async_set( + "sensor.test", "", attributes=_attributes | {ATTR_NUMERICAL_VALUE: 12} + ) await hass.async_block_till_done() expected = { "sensor.test": [ @@ -3874,7 +3966,9 @@ async def test_validate_statistics_sensor_no_longer_recorded( await assert_validation_result(client, {}) # No statistics, valid state - empty response - hass.states.async_set("sensor.test", 10, attributes=attributes) + hass.states.async_set( + "sensor.test", "", attributes=attributes | {ATTR_NUMERICAL_VALUE: 10} + ) await hass.async_block_till_done() await assert_validation_result(client, {}) @@ -3947,7 +4041,9 @@ async def test_validate_statistics_sensor_not_recorded( "homeassistant.components.sensor.recorder.is_entity_recorded", return_value=False, ): - hass.states.async_set("sensor.test", 10, attributes=attributes) + hass.states.async_set( + "sensor.test", "", attributes=attributes | {ATTR_NUMERICAL_VALUE: 10} + ) await hass.async_block_till_done() await assert_validation_result(client, expected) @@ -3993,7 +4089,9 @@ async def test_validate_statistics_sensor_removed( await assert_validation_result(client, {}) # No statistics, valid state - empty response - hass.states.async_set("sensor.test", 10, attributes=attributes) + hass.states.async_set( + "sensor.test", "", attributes=attributes | {ATTR_NUMERICAL_VALUE: 10} + ) await hass.async_block_till_done() await assert_validation_result(client, {}) @@ -4063,13 +4161,19 @@ async def test_validate_statistics_unit_change_no_conversion( # No statistics, original unit - empty response hass.states.async_set( - "sensor.test", 10, attributes={**attributes, **{"unit_of_measurement": unit1}} + "sensor.test", + "", + attributes=attributes + | {"unit_of_measurement": unit1, ATTR_NUMERICAL_VALUE: 10}, ) await assert_validation_result(client, {}) # No statistics, changed unit - empty response hass.states.async_set( - "sensor.test", 11, attributes={**attributes, **{"unit_of_measurement": unit2}} + "sensor.test", + "", + attributes=attributes + | {"unit_of_measurement": unit2, ATTR_NUMERICAL_VALUE: 11}, ) await assert_validation_result(client, {}) @@ -4081,7 +4185,10 @@ async def test_validate_statistics_unit_change_no_conversion( # No statistics, original unit - empty response hass.states.async_set( - "sensor.test", 12, attributes={**attributes, **{"unit_of_measurement": unit1}} + "sensor.test", + "", + attributes=attributes + | {"unit_of_measurement": unit1, ATTR_NUMERICAL_VALUE: 12}, ) await assert_validation_result(client, {}) @@ -4096,7 +4203,10 @@ async def test_validate_statistics_unit_change_no_conversion( # Change unit - expect error hass.states.async_set( - "sensor.test", 13, attributes={**attributes, **{"unit_of_measurement": unit2}} + "sensor.test", + "", + attributes=attributes + | {"unit_of_measurement": unit2, ATTR_NUMERICAL_VALUE: 13}, ) await async_recorder_block_till_done(hass) expected = { @@ -4193,7 +4303,10 @@ async def test_validate_statistics_unit_change_equivalent_units( # No statistics, original unit - empty response hass.states.async_set( - "sensor.test", 10, attributes={**attributes, **{"unit_of_measurement": unit1}} + "sensor.test", + "", + attributes=attributes + | {"unit_of_measurement": unit1, ATTR_NUMERICAL_VALUE: 10}, ) await assert_validation_result(client, {}) @@ -4207,7 +4320,10 @@ async def test_validate_statistics_unit_change_equivalent_units( # Units changed to an equivalent unit - empty response hass.states.async_set( - "sensor.test", 12, attributes={**attributes, **{"unit_of_measurement": unit2}} + "sensor.test", + "", + attributes=attributes + | {"unit_of_measurement": unit2, ATTR_NUMERICAL_VALUE: 12}, ) await assert_validation_result(client, {}) @@ -4273,7 +4389,10 @@ async def test_validate_statistics_unit_change_equivalent_units_2( # No statistics, original unit - empty response hass.states.async_set( - "sensor.test", 10, attributes={**attributes, **{"unit_of_measurement": unit1}} + "sensor.test", + "", + attributes=attributes + | {"unit_of_measurement": unit1, ATTR_NUMERICAL_VALUE: 10}, ) await assert_validation_result(client, {}) @@ -4287,7 +4406,10 @@ async def test_validate_statistics_unit_change_equivalent_units_2( # Units changed to an equivalent unit which is not known by the unit converters hass.states.async_set( - "sensor.test", 12, attributes={**attributes, **{"unit_of_measurement": unit2}} + "sensor.test", + "", + attributes=attributes + | {"unit_of_measurement": unit2, ATTR_NUMERICAL_VALUE: 12}, ) expected = { "sensor.test": [ @@ -4366,9 +4488,11 @@ def record_meter_states(hass, zero, entity_id, _attributes, seq): We inject a bunch of state updates for meter sensors. """ - def set_state(entity_id, state, **kwargs): + def set_state(entity_id, numerical_value, attributes): """Set the state.""" - hass.states.set(entity_id, state, **kwargs) + hass.states.set( + entity_id, "", attributes={**attributes, "numerical_value": numerical_value} + ) return hass.states.get(entity_id) one = zero + timedelta(seconds=15 * 5) # 00:01:15 @@ -4443,9 +4567,11 @@ def record_meter_state(hass, zero, entity_id, attributes, seq): We inject a state update for meter sensor. """ - def set_state(entity_id, state, **kwargs): + def set_state(entity_id, numerical_value, attributes): """Set the state.""" - hass.states.set(entity_id, state, **kwargs) + hass.states.set( + entity_id, "", attributes={**attributes, "numerical_value": numerical_value} + ) wait_recording_done(hass) return hass.states.get(entity_id) @@ -4464,9 +4590,11 @@ def record_states_partially_unavailable(hass, zero, entity_id, attributes): We inject a bunch of state updates temperature sensors. """ - def set_state(entity_id, state, **kwargs): + def set_state(entity_id, numerical_value, attributes): """Set the state.""" - hass.states.set(entity_id, state, **kwargs) + hass.states.set( + entity_id, "", attributes={**attributes, "numerical_value": numerical_value} + ) wait_recording_done(hass) return hass.states.get(entity_id)