Compare commits

...

2 Commits

Author SHA1 Message Date
2f608d8650 Use numerical_value when compiling statistics 2023-02-01 14:03:33 +01:00
65324431a4 Add numerical_value state attribute to sensor 2023-02-01 13:58:43 +01:00
6 changed files with 303 additions and 122 deletions

View File

@ -62,6 +62,7 @@ from homeassistant.util import dt as dt_util
from .const import ( # noqa: F401
ATTR_LAST_RESET,
ATTR_NUMERICAL_VALUE,
ATTR_OPTIONS,
ATTR_STATE_CLASS,
CONF_STATE_CLASS,
@ -167,6 +168,7 @@ class SensorEntity(Entity):
_last_reset_reported = False
_sensor_option_precision: int | None = None
_sensor_option_unit_of_measurement: str | None | UndefinedType = UNDEFINED
__numerical_value: int | float | None = None
@callback
def add_to_platform_start(
@ -312,6 +314,7 @@ class SensorEntity(Entity):
@property
def state_attributes(self) -> dict[str, Any] | None:
"""Return state attributes."""
attrs: dict[str, Any] = {}
if last_reset := self.last_reset:
if (
self.state_class != SensorStateClass.TOTAL
@ -334,9 +337,11 @@ class SensorEntity(Entity):
)
if self.state_class == SensorStateClass.TOTAL:
return {ATTR_LAST_RESET: last_reset.isoformat()}
attrs[ATTR_LAST_RESET] = last_reset.isoformat()
if (numerical_value := self.__numerical_value) is not None:
attrs[ATTR_NUMERICAL_VALUE] = numerical_value
return None
return attrs
@property
def native_value(self) -> StateType | date | datetime | Decimal:
@ -461,6 +466,7 @@ class SensorEntity(Entity):
@property
def state(self) -> Any: # noqa: C901
"""Return the state of the sensor and perform unit conversions, if needed."""
self.__numerical_value = None
native_unit_of_measurement = self.native_unit_of_measurement
unit_of_measurement = self.unit_of_measurement
value = self.native_value
@ -581,8 +587,8 @@ class SensorEntity(Entity):
return value
# From here on a numerical value is expected
numerical_value: int | float | Decimal
if not isinstance(value, (int, float, Decimal)):
numerical_value: int | float
if not isinstance(value, (int, float)):
try:
if isinstance(value, str) and "." not in value:
numerical_value = int(value)
@ -645,12 +651,12 @@ class SensorEntity(Entity):
)
precision = precision + floor(ratio_log)
converted_numerical_value = converter.convert(
numerical_value = converter.convert(
float(numerical_value),
native_unit_of_measurement,
unit_of_measurement,
)
value = f"{converted_numerical_value:.{precision}f}"
value = f"{numerical_value:.{precision}f}"
# This can be replaced with adding the z option when we drop support for
# Python 3.10
value = NEGATIVE_ZERO_PATTERN.sub(r"\1", value)
@ -660,6 +666,8 @@ class SensorEntity(Entity):
# Python 3.10
value = NEGATIVE_ZERO_PATTERN.sub(r"\1", value)
self.__numerical_value = numerical_value
# Validate unit of measurement used for sensors with a device class
if (
not self._invalid_unit_of_measurement_reported

View File

@ -55,6 +55,7 @@ DOMAIN: Final = "sensor"
CONF_STATE_CLASS: Final = "state_class"
ATTR_LAST_RESET: Final = "last_reset"
ATTR_NUMERICAL_VALUE: Final = "numerical_value"
ATTR_STATE_CLASS: Final = "state_class"
ATTR_OPTIONS: Final = "options"

View File

@ -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

View File

@ -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

View File

@ -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(

View File

@ -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)