From a038314d8b537bf022ac89df0b59b6c650f90e7d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 28 Nov 2022 19:46:57 +0100 Subject: [PATCH] Add display unit to WS recorder/get_statistics_metadata (#82870) --- .../components/recorder/statistics.py | 29 ++++ tests/components/demo/test_init.py | 2 + tests/components/recorder/test_statistics.py | 2 + .../components/recorder/test_websocket_api.py | 136 +++++++++++++++++- tests/components/sensor/test_recorder.py | 40 ++++++ 5 files changed, 207 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 9e1feebc682..7d7efd8b571 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -147,6 +147,30 @@ def _get_unit_class(unit: str | None) -> str | None: return None +def get_display_unit( + hass: HomeAssistant, + statistic_id: str, + statistic_unit: str | None, +) -> str | None: + """Return the unit which the statistic will be displayed in.""" + + if statistic_unit is None: + return None + + if (converter := STATISTIC_UNIT_TO_UNIT_CONVERTER.get(statistic_unit)) is None: + return statistic_unit + + state_unit: str | None = statistic_unit + if state := hass.states.get(statistic_id): + state_unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + + if state_unit == statistic_unit or state_unit not in converter.VALID_UNITS: + # Guard against invalid state unit in the DB + return statistic_unit + + return state_unit + + def _get_statistic_to_display_unit_converter( statistic_unit: str | None, state_unit: str | None, @@ -902,6 +926,9 @@ def list_statistic_ids( result = { meta["statistic_id"]: { + "display_unit_of_measurement": get_display_unit( + hass, meta["statistic_id"], meta["unit_of_measurement"] + ), "has_mean": meta["has_mean"], "has_sum": meta["has_sum"], "name": meta["name"], @@ -924,6 +951,7 @@ def list_statistic_ids( if key in result: continue result[key] = { + "display_unit_of_measurement": meta["unit_of_measurement"], "has_mean": meta["has_mean"], "has_sum": meta["has_sum"], "name": meta["name"], @@ -936,6 +964,7 @@ def list_statistic_ids( return [ { "statistic_id": _id, + "display_unit_of_measurement": info["display_unit_of_measurement"], "has_mean": info["has_mean"], "has_sum": info["has_sum"], "name": info.get("name"), diff --git a/tests/components/demo/test_init.py b/tests/components/demo/test_init.py index 7ba339a71d2..e5156f35317 100644 --- a/tests/components/demo/test_init.py +++ b/tests/components/demo/test_init.py @@ -63,6 +63,7 @@ async def test_demo_statistics(recorder_mock, mock_history, hass): list_statistic_ids, hass ) assert { + "display_unit_of_measurement": "°C", "has_mean": True, "has_sum": False, "name": "Outdoor temperature", @@ -72,6 +73,7 @@ async def test_demo_statistics(recorder_mock, mock_history, hass): "unit_class": "temperature", } in statistic_ids assert { + "display_unit_of_measurement": "kWh", "has_mean": False, "has_sum": True, "name": "Energy consumption 1", diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 26c6734178c..8950365fd95 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -554,6 +554,7 @@ async def test_import_statistics( statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ { + "display_unit_of_measurement": "kWh", "has_mean": False, "has_sum": True, "statistic_id": statistic_id, @@ -650,6 +651,7 @@ async def test_import_statistics( statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ { + "display_unit_of_measurement": "kWh", "has_mean": False, "has_sum": True, "statistic_id": statistic_id, diff --git a/tests/components/recorder/test_websocket_api.py b/tests/components/recorder/test_websocket_api.py index 855737a4d7d..fefc8dbdda1 100644 --- a/tests/components/recorder/test_websocket_api.py +++ b/tests/components/recorder/test_websocket_api.py @@ -1350,6 +1350,7 @@ async def test_list_statistic_ids( assert response["result"] == [ { "statistic_id": "sensor.test", + "display_unit_of_measurement": display_unit, "has_mean": has_mean, "has_sum": has_sum, "name": None, @@ -1371,6 +1372,7 @@ async def test_list_statistic_ids( assert response["result"] == [ { "statistic_id": "sensor.test", + "display_unit_of_measurement": display_unit, "has_mean": has_mean, "has_sum": has_sum, "name": None, @@ -1395,6 +1397,7 @@ async def test_list_statistic_ids( assert response["result"] == [ { "statistic_id": "sensor.test", + "display_unit_of_measurement": display_unit, "has_mean": has_mean, "has_sum": has_sum, "name": None, @@ -1415,6 +1418,7 @@ async def test_list_statistic_ids( assert response["result"] == [ { "statistic_id": "sensor.test", + "display_unit_of_measurement": display_unit, "has_mean": has_mean, "has_sum": has_sum, "name": None, @@ -1427,6 +1431,121 @@ async def test_list_statistic_ids( assert response["result"] == [] +@pytest.mark.parametrize( + "attributes, attributes2, display_unit, statistics_unit, unit_class", + [ + ( + DISTANCE_SENSOR_M_ATTRIBUTES, + DISTANCE_SENSOR_FT_ATTRIBUTES, + "ft", + "m", + "distance", + ), + ( + ENERGY_SENSOR_WH_ATTRIBUTES, + ENERGY_SENSOR_KWH_ATTRIBUTES, + "kWh", + "Wh", + "energy", + ), + (GAS_SENSOR_FT3_ATTRIBUTES, GAS_SENSOR_M3_ATTRIBUTES, "m³", "ft³", "volume"), + (POWER_SENSOR_KW_ATTRIBUTES, POWER_SENSOR_W_ATTRIBUTES, "W", "kW", "power"), + ( + PRESSURE_SENSOR_HPA_ATTRIBUTES, + PRESSURE_SENSOR_PA_ATTRIBUTES, + "Pa", + "hPa", + "pressure", + ), + ( + SPEED_SENSOR_KPH_ATTRIBUTES, + SPEED_SENSOR_MPH_ATTRIBUTES, + "mph", + "km/h", + "speed", + ), + ( + TEMPERATURE_SENSOR_C_ATTRIBUTES, + TEMPERATURE_SENSOR_F_ATTRIBUTES, + "°F", + "°C", + "temperature", + ), + ( + VOLUME_SENSOR_FT3_ATTRIBUTES, + VOLUME_SENSOR_M3_ATTRIBUTES, + "m³", + "ft³", + "volume", + ), + ], +) +async def test_list_statistic_ids_unit_change( + recorder_mock, + hass, + hass_ws_client, + attributes, + attributes2, + display_unit, + statistics_unit, + unit_class, +): + """Test list_statistic_ids.""" + now = dt_util.utcnow() + has_mean = attributes["state_class"] == "measurement" + has_sum = not has_mean + + await async_setup_component(hass, "sensor", {}) + await async_recorder_block_till_done(hass) + + client = await hass_ws_client() + await client.send_json({"id": 1, "type": "recorder/list_statistic_ids"}) + response = await client.receive_json() + assert response["success"] + assert response["result"] == [] + + hass.states.async_set("sensor.test", 10, attributes=attributes) + await async_wait_recording_done(hass) + + do_adhoc_statistics(hass, start=now) + await async_recorder_block_till_done(hass) + + await client.send_json({"id": 2, "type": "recorder/list_statistic_ids"}) + response = await client.receive_json() + assert response["success"] + assert response["result"] == [ + { + "statistic_id": "sensor.test", + "display_unit_of_measurement": statistics_unit, + "has_mean": has_mean, + "has_sum": has_sum, + "name": None, + "source": "recorder", + "statistics_unit_of_measurement": statistics_unit, + "unit_class": unit_class, + } + ] + + # Change the state unit + hass.states.async_set("sensor.test", 10, attributes=attributes2) + + await client.send_json({"id": 3, "type": "recorder/list_statistic_ids"}) + response = await client.receive_json() + assert response["success"] + assert response["result"] == [ + { + "statistic_id": "sensor.test", + "display_unit_of_measurement": display_unit, + "has_mean": has_mean, + "has_sum": has_sum, + "name": None, + "source": "recorder", + "statistics_unit_of_measurement": statistics_unit, + "unit_class": unit_class, + } + ] + + async def test_validate_statistics(recorder_mock, hass, hass_ws_client): """Test validate_statistics can be called.""" id = 1 @@ -1570,10 +1689,11 @@ async def test_clear_statistics(recorder_mock, hass, hass_ws_client): @pytest.mark.parametrize( - "new_unit, new_unit_class", [("dogs", None), (None, None), ("W", "power")] + "new_unit, new_unit_class, new_display_unit", + [("dogs", None, "dogs"), (None, None, None), ("W", "power", "kW")], ) async def test_update_statistics_metadata( - recorder_mock, hass, hass_ws_client, new_unit, new_unit_class + recorder_mock, hass, hass_ws_client, new_unit, new_unit_class, new_display_unit ): """Test removing statistics.""" now = dt_util.utcnow() @@ -1599,6 +1719,7 @@ async def test_update_statistics_metadata( assert response["result"] == [ { "statistic_id": "sensor.test", + "display_unit_of_measurement": "kW", "has_mean": True, "has_sum": False, "name": None, @@ -1626,6 +1747,7 @@ async def test_update_statistics_metadata( assert response["result"] == [ { "statistic_id": "sensor.test", + "display_unit_of_measurement": new_display_unit, "has_mean": True, "has_sum": False, "name": None, @@ -1688,6 +1810,7 @@ async def test_change_statistics_unit(recorder_mock, hass, hass_ws_client): assert response["result"] == [ { "statistic_id": "sensor.test", + "display_unit_of_measurement": "kW", "has_mean": True, "has_sum": False, "name": None, @@ -1742,6 +1865,7 @@ async def test_change_statistics_unit(recorder_mock, hass, hass_ws_client): assert response["result"] == [ { "statistic_id": "sensor.test", + "display_unit_of_measurement": "kW", "has_mean": True, "has_sum": False, "name": None, @@ -1793,6 +1917,7 @@ async def test_change_statistics_unit_errors( expected_statistic_ids = [ { "statistic_id": "sensor.test", + "display_unit_of_measurement": "kW", "has_mean": True, "has_sum": False, "name": None, @@ -2177,6 +2302,7 @@ async def test_get_statistics_metadata( assert response["result"] == [ { "statistic_id": "test:total_gas", + "display_unit_of_measurement": unit, "has_mean": has_mean, "has_sum": has_sum, "name": "Total imported energy", @@ -2204,6 +2330,7 @@ async def test_get_statistics_metadata( assert response["result"] == [ { "statistic_id": "sensor.test", + "display_unit_of_measurement": attributes["unit_of_measurement"], "has_mean": has_mean, "has_sum": has_sum, "name": None, @@ -2231,6 +2358,7 @@ async def test_get_statistics_metadata( assert response["result"] == [ { "statistic_id": "sensor.test", + "display_unit_of_measurement": attributes["unit_of_measurement"], "has_mean": has_mean, "has_sum": has_sum, "name": None, @@ -2324,6 +2452,7 @@ async def test_import_statistics( statistic_ids = list_statistic_ids(hass) # TODO assert statistic_ids == [ { + "display_unit_of_measurement": "kWh", "has_mean": False, "has_sum": True, "statistic_id": statistic_id, @@ -2550,6 +2679,7 @@ async def test_adjust_sum_statistics_energy( statistic_ids = list_statistic_ids(hass) # TODO assert statistic_ids == [ { + "display_unit_of_measurement": "kWh", "has_mean": False, "has_sum": True, "statistic_id": statistic_id, @@ -2740,6 +2870,7 @@ async def test_adjust_sum_statistics_gas( statistic_ids = list_statistic_ids(hass) # TODO assert statistic_ids == [ { + "display_unit_of_measurement": "m³", "has_mean": False, "has_sum": True, "statistic_id": statistic_id, @@ -2946,6 +3077,7 @@ async def test_adjust_sum_statistics_errors( statistic_ids = list_statistic_ids(hass) assert statistic_ids == [ { + "display_unit_of_measurement": state_unit, "has_mean": False, "has_sum": True, "statistic_id": statistic_id, diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 83d61d0a7fc..1e129b7af92 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -141,6 +141,7 @@ def test_compile_hourly_statistics( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": display_unit, "has_mean": True, "has_sum": False, "name": None, @@ -214,6 +215,7 @@ def test_compile_hourly_statistics_purged_state_changes( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": display_unit, "has_mean": True, "has_sum": False, "name": None, @@ -282,6 +284,7 @@ def test_compile_hourly_statistics_wrong_unit(hass_recorder, caplog, attributes) assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": "°C", "has_mean": True, "has_sum": False, "name": None, @@ -290,6 +293,7 @@ def test_compile_hourly_statistics_wrong_unit(hass_recorder, caplog, attributes) "unit_class": "temperature", }, { + "display_unit_of_measurement": "invalid", "has_mean": True, "has_sum": False, "name": None, @@ -299,6 +303,7 @@ def test_compile_hourly_statistics_wrong_unit(hass_recorder, caplog, attributes) "unit_class": None, }, { + "display_unit_of_measurement": None, "has_mean": True, "has_sum": False, "name": None, @@ -309,6 +314,7 @@ def test_compile_hourly_statistics_wrong_unit(hass_recorder, caplog, attributes) }, { "statistic_id": "sensor.test6", + "display_unit_of_measurement": "°C", "has_mean": True, "has_sum": False, "name": None, @@ -318,6 +324,7 @@ def test_compile_hourly_statistics_wrong_unit(hass_recorder, caplog, attributes) }, { "statistic_id": "sensor.test7", + "display_unit_of_measurement": "°C", "has_mean": True, "has_sum": False, "name": None, @@ -473,6 +480,7 @@ async def test_compile_hourly_sum_statistics_amount( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": statistics_unit, "has_mean": False, "has_sum": True, "name": None, @@ -662,6 +670,7 @@ def test_compile_hourly_sum_statistics_amount_reset_every_state_change( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": display_unit, "has_mean": False, "has_sum": True, "name": None, @@ -758,6 +767,7 @@ def test_compile_hourly_sum_statistics_amount_invalid_last_reset( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": display_unit, "has_mean": False, "has_sum": True, "name": None, @@ -841,6 +851,7 @@ def test_compile_hourly_sum_statistics_nan_inf_state( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": display_unit, "has_mean": False, "has_sum": True, "name": None, @@ -965,6 +976,7 @@ def test_compile_hourly_sum_statistics_negative_state( wait_recording_done(hass) statistic_ids = list_statistic_ids(hass) assert { + "display_unit_of_measurement": display_unit, "has_mean": False, "has_sum": True, "name": None, @@ -1052,6 +1064,7 @@ def test_compile_hourly_sum_statistics_total_no_reset( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": display_unit, "has_mean": False, "has_sum": True, "name": None, @@ -1151,6 +1164,7 @@ def test_compile_hourly_sum_statistics_total_increasing( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": display_unit, "has_mean": False, "has_sum": True, "name": None, @@ -1261,6 +1275,7 @@ def test_compile_hourly_sum_statistics_total_increasing_small_dip( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": display_unit, "has_mean": False, "has_sum": True, "name": None, @@ -1352,6 +1367,7 @@ def test_compile_hourly_energy_statistics_unsupported(hass_recorder, caplog): assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": "kWh", "has_mean": False, "has_sum": True, "name": None, @@ -1441,6 +1457,7 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": "kWh", "has_mean": False, "has_sum": True, "name": None, @@ -1450,6 +1467,7 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): }, { "statistic_id": "sensor.test2", + "display_unit_of_measurement": "kWh", "has_mean": False, "has_sum": True, "name": None, @@ -1459,6 +1477,7 @@ def test_compile_hourly_energy_statistics_multiple(hass_recorder, caplog): }, { "statistic_id": "sensor.test3", + "display_unit_of_measurement": "Wh", "has_mean": False, "has_sum": True, "name": None, @@ -1814,6 +1833,7 @@ def test_list_statistic_ids( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": display_unit, "has_mean": statistic_type == "mean", "has_sum": statistic_type == "sum", "name": None, @@ -1828,6 +1848,7 @@ def test_list_statistic_ids( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": display_unit, "has_mean": statistic_type == "mean", "has_sum": statistic_type == "sum", "name": None, @@ -1921,6 +1942,7 @@ def test_compile_hourly_statistics_changing_units_1( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": state_unit, "has_mean": True, "has_sum": False, "name": None, @@ -1955,6 +1977,7 @@ def test_compile_hourly_statistics_changing_units_1( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": state_unit, "has_mean": True, "has_sum": False, "name": None, @@ -2029,6 +2052,7 @@ def test_compile_hourly_statistics_changing_units_2( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": "cats", "has_mean": True, "has_sum": False, "name": None, @@ -2094,6 +2118,7 @@ def test_compile_hourly_statistics_changing_units_3( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": display_unit, "has_mean": True, "has_sum": False, "name": None, @@ -2128,6 +2153,7 @@ def test_compile_hourly_statistics_changing_units_3( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": display_unit, "has_mean": True, "has_sum": False, "name": None, @@ -2206,6 +2232,7 @@ def test_compile_hourly_statistics_equivalent_units_1( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": state_unit, "has_mean": True, "has_sum": False, "name": None, @@ -2236,6 +2263,7 @@ def test_compile_hourly_statistics_equivalent_units_1( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": state_unit2, "has_mean": True, "has_sum": False, "name": None, @@ -2319,6 +2347,7 @@ def test_compile_hourly_statistics_equivalent_units_2( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": state_unit, "has_mean": True, "has_sum": False, "name": None, @@ -2384,6 +2413,7 @@ def test_compile_hourly_statistics_changing_device_class_1( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": state_unit, "has_mean": True, "has_sum": False, "name": None, @@ -2428,6 +2458,7 @@ def test_compile_hourly_statistics_changing_device_class_1( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": state_unit, "has_mean": True, "has_sum": False, "name": None, @@ -2482,6 +2513,7 @@ def test_compile_hourly_statistics_changing_device_class_1( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": state_unit, "has_mean": True, "has_sum": False, "name": None, @@ -2568,6 +2600,7 @@ def test_compile_hourly_statistics_changing_device_class_2( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": display_unit, "has_mean": True, "has_sum": False, "name": None, @@ -2612,6 +2645,7 @@ def test_compile_hourly_statistics_changing_device_class_2( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": display_unit, "has_mean": True, "has_sum": False, "name": None, @@ -2690,6 +2724,7 @@ def test_compile_hourly_statistics_changing_statistics( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": None, "has_mean": True, "has_sum": False, "name": None, @@ -2725,6 +2760,7 @@ def test_compile_hourly_statistics_changing_statistics( assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": None, "has_mean": False, "has_sum": True, "name": None, @@ -2916,6 +2952,7 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog): assert statistic_ids == [ { "statistic_id": "sensor.test1", + "display_unit_of_measurement": "%", "has_mean": True, "has_sum": False, "name": None, @@ -2925,6 +2962,7 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog): }, { "statistic_id": "sensor.test2", + "display_unit_of_measurement": "%", "has_mean": True, "has_sum": False, "name": None, @@ -2934,6 +2972,7 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog): }, { "statistic_id": "sensor.test3", + "display_unit_of_measurement": "%", "has_mean": True, "has_sum": False, "name": None, @@ -2943,6 +2982,7 @@ def test_compile_statistics_hourly_daily_monthly_summary(hass_recorder, caplog): }, { "statistic_id": "sensor.test4", + "display_unit_of_measurement": "EUR", "has_mean": False, "has_sum": True, "name": None,