diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index fd6cf5e0f2f..c485622af80 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -637,35 +637,70 @@ def validate_statistics( """Validate statistics.""" validation_result = defaultdict(list) - sensor_states = _get_sensor_states(hass) + sensor_states = hass.states.all(DOMAIN) + metadatas = statistics.get_metadata(hass, [i.entity_id for i in sensor_states]) for state in sensor_states: entity_id = state.entity_id device_class = state.attributes.get(ATTR_DEVICE_CLASS) + state_class = state.attributes.get(ATTR_STATE_CLASS) state_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) - if device_class not in UNIT_CONVERSIONS: - metadata = statistics.get_metadata(hass, (entity_id,)) - if not metadata: - continue - metadata_unit = metadata[entity_id][1]["unit_of_measurement"] - if state_unit != metadata_unit: + if metadata := metadatas.get(entity_id): + if not is_entity_recorded(hass, state.entity_id): + # Sensor was previously recorded, but no longer is validation_result[entity_id].append( statistics.ValidationIssue( - "units_changed", + "entity_not_recorded", + {"statistic_id": entity_id}, + ) + ) + + if state_class not in STATE_CLASSES: + # Sensor no longer has a valid state class + validation_result[entity_id].append( + statistics.ValidationIssue( + "unsupported_state_class", + {"statistic_id": entity_id, "state_class": state_class}, + ) + ) + + metadata_unit = metadata[1]["unit_of_measurement"] + if device_class not in UNIT_CONVERSIONS: + if state_unit != metadata_unit: + # The unit has changed + validation_result[entity_id].append( + statistics.ValidationIssue( + "units_changed", + { + "statistic_id": entity_id, + "state_unit": state_unit, + "metadata_unit": metadata_unit, + }, + ) + ) + elif metadata_unit != DEVICE_CLASS_UNITS[device_class]: + # The unit in metadata is not supported for this device class + validation_result[entity_id].append( + statistics.ValidationIssue( + "unsupported_unit_metadata", { "statistic_id": entity_id, - "state_unit": state_unit, + "device_class": device_class, "metadata_unit": metadata_unit, + "supported_unit": DEVICE_CLASS_UNITS[device_class], }, ) ) - continue - if state_unit not in UNIT_CONVERSIONS[device_class]: + if ( + device_class in UNIT_CONVERSIONS + and state_unit not in UNIT_CONVERSIONS[device_class] + ): + # The unit in the state is not supported for this device class validation_result[entity_id].append( statistics.ValidationIssue( - "unsupported_unit", + "unsupported_unit_state", { "statistic_id": entity_id, "device_class": device_class, diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 4f54a43ca6e..e60659aaab2 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -6,34 +6,19 @@ import pytest from pytest import approx from homeassistant.components.recorder.const import DATA_INSTANCE -from homeassistant.components.recorder.models import StatisticsMeta -from homeassistant.components.recorder.util import session_scope from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM +from homeassistant.util.unit_system import METRIC_SYSTEM from .common import trigger_db_commit from tests.common import init_recorder_component -BATTERY_SENSOR_ATTRIBUTES = { - "device_class": "battery", - "state_class": "measurement", - "unit_of_measurement": "%", -} POWER_SENSOR_ATTRIBUTES = { "device_class": "power", "state_class": "measurement", "unit_of_measurement": "kW", } -NONE_SENSOR_ATTRIBUTES = { - "state_class": "measurement", -} -PRESSURE_SENSOR_ATTRIBUTES = { - "device_class": "pressure", - "state_class": "measurement", - "unit_of_measurement": "hPa", -} TEMPERATURE_SENSOR_ATTRIBUTES = { "device_class": "temperature", "state_class": "measurement", @@ -41,21 +26,8 @@ TEMPERATURE_SENSOR_ATTRIBUTES = { } -@pytest.mark.parametrize( - "units, attributes, unit", - [ - (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W"), - (METRIC_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W"), - (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, "°F"), - (METRIC_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, "°C"), - (IMPERIAL_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, "psi"), - (METRIC_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, "Pa"), - ], -) -async def test_validate_statistics_supported_device_class( - hass, hass_ws_client, units, attributes, unit -): - """Test list_statistic_ids.""" +async def test_validate_statistics(hass, hass_ws_client): + """Test validate_statistics can be called.""" id = 1 def next_id(): @@ -71,177 +43,9 @@ async def test_validate_statistics_supported_device_class( assert response["success"] assert response["result"] == expected_result - now = dt_util.utcnow() - - hass.config.units = units - await hass.async_add_executor_job(init_recorder_component, hass) - await async_setup_component(hass, "sensor", {}) - await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) - client = await hass_ws_client() - # No statistics, no state - empty response - await assert_validation_result(client, {}) - - # No statistics, valid state - empty response - hass.states.async_set( - "sensor.test", 10, attributes={**attributes, **{"unit_of_measurement": unit}} - ) - await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) - await assert_validation_result(client, {}) - - # No statistics, invalid state - expect error - hass.states.async_set( - "sensor.test", 11, attributes={**attributes, **{"unit_of_measurement": "dogs"}} - ) - await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) - expected = { - "sensor.test": [ - { - "data": { - "device_class": attributes["device_class"], - "state_unit": "dogs", - "statistic_id": "sensor.test", - }, - "type": "unsupported_unit", - } - ], - } - await assert_validation_result(client, expected) - - # Statistics has run, invalid state - expect error - await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) - hass.data[DATA_INSTANCE].do_adhoc_statistics(start=now) - hass.states.async_set( - "sensor.test", 12, attributes={**attributes, **{"unit_of_measurement": "dogs"}} - ) - await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) - await assert_validation_result(client, expected) - - # Valid state - empty response - hass.states.async_set( - "sensor.test", 13, attributes={**attributes, **{"unit_of_measurement": unit}} - ) - await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) - await assert_validation_result(client, {}) - - # Valid state, statistic runs again - empty response - hass.data[DATA_INSTANCE].do_adhoc_statistics(start=now) - await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) - await assert_validation_result(client, {}) - - # Remove the state - empty response - hass.states.async_remove("sensor.test") - await assert_validation_result(client, {}) - - -@pytest.mark.parametrize( - "attributes", - [BATTERY_SENSOR_ATTRIBUTES, NONE_SENSOR_ATTRIBUTES], -) -async def test_validate_statistics_unsupported_device_class( - hass, hass_ws_client, attributes -): - """Test list_statistic_ids.""" - id = 1 - - def next_id(): - nonlocal id - id += 1 - return id - - async def assert_validation_result(client, expected_result): - await client.send_json( - {"id": next_id(), "type": "recorder/validate_statistics"} - ) - response = await client.receive_json() - assert response["success"] - assert response["result"] == expected_result - - async def assert_statistic_ids(expected_result): - with session_scope(hass=hass) as session: - db_states = list(session.query(StatisticsMeta)) - assert len(db_states) == len(expected_result) - for i in range(len(db_states)): - assert db_states[i].statistic_id == expected_result[i]["statistic_id"] - assert ( - db_states[i].unit_of_measurement - == expected_result[i]["unit_of_measurement"] - ) - - now = dt_util.utcnow() - await hass.async_add_executor_job(init_recorder_component, hass) - await async_setup_component(hass, "sensor", {}) - await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) client = await hass_ws_client() - rec = hass.data[DATA_INSTANCE] - - # No statistics, no state - empty response - await assert_validation_result(client, {}) - - # No statistics, original unit - empty response - hass.states.async_set("sensor.test", 10, attributes=attributes) - await assert_validation_result(client, {}) - - # No statistics, changed unit - empty response - hass.states.async_set( - "sensor.test", 11, attributes={**attributes, **{"unit_of_measurement": "dogs"}} - ) - await assert_validation_result(client, {}) - - # Run statistics, no statistics will be generated because of conflicting units - await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) - rec.do_adhoc_statistics(start=now) - await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) - await assert_statistic_ids([]) - - # No statistics, changed unit - empty response - hass.states.async_set( - "sensor.test", 12, attributes={**attributes, **{"unit_of_measurement": "dogs"}} - ) - await assert_validation_result(client, {}) - - # Run statistics one hour later, only the "dogs" state will be considered - await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) - rec.do_adhoc_statistics(start=now + timedelta(hours=1)) - await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) - await assert_statistic_ids( - [{"statistic_id": "sensor.test", "unit_of_measurement": "dogs"}] - ) - await assert_validation_result(client, {}) - - # Change back to original unit - expect error - hass.states.async_set("sensor.test", 13, attributes=attributes) - await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) - expected = { - "sensor.test": [ - { - "data": { - "metadata_unit": "dogs", - "state_unit": attributes.get("unit_of_measurement"), - "statistic_id": "sensor.test", - }, - "type": "units_changed", - } - ], - } - await assert_validation_result(client, expected) - - # Changed unit - empty response - hass.states.async_set( - "sensor.test", 14, attributes={**attributes, **{"unit_of_measurement": "dogs"}} - ) - await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) - await assert_validation_result(client, {}) - - # Valid state, statistic runs again - empty response - await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) - hass.data[DATA_INSTANCE].do_adhoc_statistics(start=now) - await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) - await assert_validation_result(client, {}) - - # Remove the state - empty response - hass.states.async_remove("sensor.test") await assert_validation_result(client, {}) diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index f13dc5084bb..9ae4b467da5 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -11,24 +11,37 @@ from pytest import approx from homeassistant import loader from homeassistant.components.recorder import history from homeassistant.components.recorder.const import DATA_INSTANCE -from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat +from homeassistant.components.recorder.models import ( + StatisticsMeta, + process_timestamp_to_utc_isoformat, +) from homeassistant.components.recorder.statistics import ( get_metadata, list_statistic_ids, statistics_during_period, ) +from homeassistant.components.recorder.util import session_scope from homeassistant.const import STATE_UNAVAILABLE from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM +from tests.common import async_setup_component, init_recorder_component from tests.components.recorder.common import wait_recording_done +BATTERY_SENSOR_ATTRIBUTES = { + "device_class": "battery", + "state_class": "measurement", + "unit_of_measurement": "%", +} ENERGY_SENSOR_ATTRIBUTES = { "device_class": "energy", "state_class": "measurement", "unit_of_measurement": "kWh", } +NONE_SENSOR_ATTRIBUTES = { + "state_class": "measurement", +} POWER_SENSOR_ATTRIBUTES = { "device_class": "power", "state_class": "measurement", @@ -2080,6 +2093,428 @@ def record_states(hass, zero, entity_id, attributes, seq=None): return four, states +@pytest.mark.parametrize( + "units, attributes, unit", + [ + (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W"), + (METRIC_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W"), + (IMPERIAL_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, "°F"), + (METRIC_SYSTEM, TEMPERATURE_SENSOR_ATTRIBUTES, "°C"), + (IMPERIAL_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, "psi"), + (METRIC_SYSTEM, PRESSURE_SENSOR_ATTRIBUTES, "Pa"), + ], +) +async def test_validate_statistics_supported_device_class( + hass, hass_ws_client, units, attributes, unit +): + """Test validate_statistics.""" + id = 1 + + def next_id(): + nonlocal id + id += 1 + return id + + async def assert_validation_result(client, expected_result): + await client.send_json( + {"id": next_id(), "type": "recorder/validate_statistics"} + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == expected_result + + now = dt_util.utcnow() + + hass.config.units = units + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component(hass, "sensor", {}) + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + client = await hass_ws_client() + + # No statistics, no state - empty response + await assert_validation_result(client, {}) + + # No statistics, valid state - empty response + hass.states.async_set( + "sensor.test", 10, attributes={**attributes, **{"unit_of_measurement": unit}} + ) + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + await assert_validation_result(client, {}) + + # No statistics, invalid state - expect error + hass.states.async_set( + "sensor.test", 11, attributes={**attributes, **{"unit_of_measurement": "dogs"}} + ) + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + expected = { + "sensor.test": [ + { + "data": { + "device_class": attributes["device_class"], + "state_unit": "dogs", + "statistic_id": "sensor.test", + }, + "type": "unsupported_unit_state", + } + ], + } + await assert_validation_result(client, expected) + + # Statistics has run, invalid state - expect error + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + hass.data[DATA_INSTANCE].do_adhoc_statistics(start=now) + hass.states.async_set( + "sensor.test", 12, attributes={**attributes, **{"unit_of_measurement": "dogs"}} + ) + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + await assert_validation_result(client, expected) + + # Valid state - empty response + hass.states.async_set( + "sensor.test", 13, attributes={**attributes, **{"unit_of_measurement": unit}} + ) + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + await assert_validation_result(client, {}) + + # Valid state, statistic runs again - empty response + hass.data[DATA_INSTANCE].do_adhoc_statistics(start=now) + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + await assert_validation_result(client, {}) + + # Remove the state - empty response + hass.states.async_remove("sensor.test") + await assert_validation_result(client, {}) + + +@pytest.mark.parametrize( + "units, attributes, unit", + [ + (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W"), + ], +) +async def test_validate_statistics_supported_device_class_2( + hass, hass_ws_client, units, attributes, unit +): + """Test validate_statistics.""" + id = 1 + + def next_id(): + nonlocal id + id += 1 + return id + + async def assert_validation_result(client, expected_result): + await client.send_json( + {"id": next_id(), "type": "recorder/validate_statistics"} + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == expected_result + + now = dt_util.utcnow() + + hass.config.units = units + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component(hass, "sensor", {}) + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + client = await hass_ws_client() + + # No statistics, no state - empty response + await assert_validation_result(client, {}) + + # No statistics, valid state - empty response + initial_attributes = {"state_class": "measurement"} + hass.states.async_set("sensor.test", 10, attributes=initial_attributes) + await hass.async_block_till_done() + await assert_validation_result(client, {}) + + # Statistics has run, device class set - expect error + hass.data[DATA_INSTANCE].do_adhoc_statistics(start=now) + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + hass.states.async_set("sensor.test", 12, attributes=attributes) + await hass.async_block_till_done() + expected = { + "sensor.test": [ + { + "data": { + "device_class": attributes["device_class"], + "metadata_unit": None, + "statistic_id": "sensor.test", + "supported_unit": unit, + }, + "type": "unsupported_unit_metadata", + } + ], + } + await assert_validation_result(client, expected) + + # Invalid state too, expect double errors + hass.states.async_set( + "sensor.test", 13, attributes={**attributes, **{"unit_of_measurement": "dogs"}} + ) + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + expected = { + "sensor.test": [ + { + "data": { + "device_class": attributes["device_class"], + "metadata_unit": None, + "statistic_id": "sensor.test", + "supported_unit": unit, + }, + "type": "unsupported_unit_metadata", + }, + { + "data": { + "device_class": attributes["device_class"], + "state_unit": "dogs", + "statistic_id": "sensor.test", + }, + "type": "unsupported_unit_state", + }, + ], + } + await assert_validation_result(client, expected) + + +@pytest.mark.parametrize( + "units, attributes, unit", + [ + (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W"), + ], +) +async def test_validate_statistics_unsupported_state_class( + hass, hass_ws_client, units, attributes, unit +): + """Test validate_statistics.""" + id = 1 + + def next_id(): + nonlocal id + id += 1 + return id + + async def assert_validation_result(client, expected_result): + await client.send_json( + {"id": next_id(), "type": "recorder/validate_statistics"} + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == expected_result + + now = dt_util.utcnow() + + hass.config.units = units + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component(hass, "sensor", {}) + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + client = await hass_ws_client() + + # No statistics, no state - empty response + await assert_validation_result(client, {}) + + # No statistics, valid state - empty response + hass.states.async_set("sensor.test", 10, attributes=attributes) + await hass.async_block_till_done() + await assert_validation_result(client, {}) + + # Statistics has run, empty response + hass.data[DATA_INSTANCE].do_adhoc_statistics(start=now) + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + await assert_validation_result(client, {}) + + # State update with invalid state class, expect error + _attributes = dict(attributes) + _attributes.pop("state_class") + hass.states.async_set("sensor.test", 12, attributes=_attributes) + await hass.async_block_till_done() + expected = { + "sensor.test": [ + { + "data": { + "state_class": None, + "statistic_id": "sensor.test", + }, + "type": "unsupported_state_class", + } + ], + } + await assert_validation_result(client, expected) + + +@pytest.mark.parametrize( + "units, attributes, unit", + [ + (IMPERIAL_SYSTEM, POWER_SENSOR_ATTRIBUTES, "W"), + ], +) +async def test_validate_statistics_sensor_not_recorded( + hass, hass_ws_client, units, attributes, unit +): + """Test validate_statistics.""" + id = 1 + + def next_id(): + nonlocal id + id += 1 + return id + + async def assert_validation_result(client, expected_result): + await client.send_json( + {"id": next_id(), "type": "recorder/validate_statistics"} + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == expected_result + + now = dt_util.utcnow() + + hass.config.units = units + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component(hass, "sensor", {}) + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + client = await hass_ws_client() + + # No statistics, no state - empty response + await assert_validation_result(client, {}) + + # No statistics, valid state - empty response + hass.states.async_set("sensor.test", 10, attributes=attributes) + await hass.async_block_till_done() + await assert_validation_result(client, {}) + + # Statistics has run, empty response + hass.data[DATA_INSTANCE].do_adhoc_statistics(start=now) + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + await assert_validation_result(client, {}) + + # Sensor no longer recorded, expect error + expected = { + "sensor.test": [ + { + "data": {"statistic_id": "sensor.test"}, + "type": "entity_not_recorded", + } + ], + } + with patch( + "homeassistant.components.sensor.recorder.is_entity_recorded", + return_value=False, + ): + await assert_validation_result(client, expected) + + +@pytest.mark.parametrize( + "attributes", + [BATTERY_SENSOR_ATTRIBUTES, NONE_SENSOR_ATTRIBUTES], +) +async def test_validate_statistics_unsupported_device_class( + hass, hass_ws_client, attributes +): + """Test validate_statistics.""" + id = 1 + + def next_id(): + nonlocal id + id += 1 + return id + + async def assert_validation_result(client, expected_result): + await client.send_json( + {"id": next_id(), "type": "recorder/validate_statistics"} + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == expected_result + + async def assert_statistic_ids(expected_result): + with session_scope(hass=hass) as session: + db_states = list(session.query(StatisticsMeta)) + assert len(db_states) == len(expected_result) + for i in range(len(db_states)): + assert db_states[i].statistic_id == expected_result[i]["statistic_id"] + assert ( + db_states[i].unit_of_measurement + == expected_result[i]["unit_of_measurement"] + ) + + now = dt_util.utcnow() + + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component(hass, "sensor", {}) + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + client = await hass_ws_client() + rec = hass.data[DATA_INSTANCE] + + # No statistics, no state - empty response + await assert_validation_result(client, {}) + + # No statistics, original unit - empty response + hass.states.async_set("sensor.test", 10, attributes=attributes) + await assert_validation_result(client, {}) + + # No statistics, changed unit - empty response + hass.states.async_set( + "sensor.test", 11, attributes={**attributes, **{"unit_of_measurement": "dogs"}} + ) + await assert_validation_result(client, {}) + + # Run statistics, no statistics will be generated because of conflicting units + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + rec.do_adhoc_statistics(start=now) + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + await assert_statistic_ids([]) + + # No statistics, changed unit - empty response + hass.states.async_set( + "sensor.test", 12, attributes={**attributes, **{"unit_of_measurement": "dogs"}} + ) + await assert_validation_result(client, {}) + + # Run statistics one hour later, only the "dogs" state will be considered + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + rec.do_adhoc_statistics(start=now + timedelta(hours=1)) + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + await assert_statistic_ids( + [{"statistic_id": "sensor.test", "unit_of_measurement": "dogs"}] + ) + await assert_validation_result(client, {}) + + # Change back to original unit - expect error + hass.states.async_set("sensor.test", 13, attributes=attributes) + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + expected = { + "sensor.test": [ + { + "data": { + "metadata_unit": "dogs", + "state_unit": attributes.get("unit_of_measurement"), + "statistic_id": "sensor.test", + }, + "type": "units_changed", + } + ], + } + await assert_validation_result(client, expected) + + # Changed unit - empty response + hass.states.async_set( + "sensor.test", 14, attributes={**attributes, **{"unit_of_measurement": "dogs"}} + ) + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + await assert_validation_result(client, {}) + + # Valid state, statistic runs again - empty response + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + hass.data[DATA_INSTANCE].do_adhoc_statistics(start=now) + await hass.async_add_executor_job(hass.data[DATA_INSTANCE].block_till_done) + await assert_validation_result(client, {}) + + # Remove the state - empty response + hass.states.async_remove("sensor.test") + await assert_validation_result(client, {}) + + def record_meter_states(hass, zero, entity_id, _attributes, seq): """Record some test states.