mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
Let the statistics component calculate changes in fossil energy consumption calculation (#101557)
This commit is contained in:
parent
235a3486ee
commit
017c699e19
@ -274,10 +274,10 @@ async def ws_get_fossil_energy_consumption(
|
|||||||
statistic_ids,
|
statistic_ids,
|
||||||
"hour",
|
"hour",
|
||||||
{"energy": UnitOfEnergy.KILO_WATT_HOUR},
|
{"energy": UnitOfEnergy.KILO_WATT_HOUR},
|
||||||
{"mean", "sum"},
|
{"mean", "change"},
|
||||||
)
|
)
|
||||||
|
|
||||||
def _combine_sum_statistics(
|
def _combine_change_statistics(
|
||||||
stats: dict[str, list[StatisticsRow]], statistic_ids: list[str]
|
stats: dict[str, list[StatisticsRow]], statistic_ids: list[str]
|
||||||
) -> dict[float, float]:
|
) -> dict[float, float]:
|
||||||
"""Combine multiple statistics, returns a dict indexed by start time."""
|
"""Combine multiple statistics, returns a dict indexed by start time."""
|
||||||
@ -287,21 +287,12 @@ async def ws_get_fossil_energy_consumption(
|
|||||||
if statistics_id not in statistic_ids:
|
if statistics_id not in statistic_ids:
|
||||||
continue
|
continue
|
||||||
for period in stat:
|
for period in stat:
|
||||||
if period["sum"] is None:
|
if period["change"] is None:
|
||||||
continue
|
continue
|
||||||
result[period["start"]] += period["sum"]
|
result[period["start"]] += period["change"]
|
||||||
|
|
||||||
return {key: result[key] for key in sorted(result)}
|
return {key: result[key] for key in sorted(result)}
|
||||||
|
|
||||||
def _calculate_deltas(sums: dict[float, float]) -> dict[float, float]:
|
|
||||||
prev: float | None = None
|
|
||||||
result: dict[float, float] = {}
|
|
||||||
for period, sum_ in sums.items():
|
|
||||||
if prev is not None:
|
|
||||||
result[period] = sum_ - prev
|
|
||||||
prev = sum_
|
|
||||||
return result
|
|
||||||
|
|
||||||
def _reduce_deltas(
|
def _reduce_deltas(
|
||||||
stat_list: list[dict[str, Any]],
|
stat_list: list[dict[str, Any]],
|
||||||
same_period: Callable[[float, float], bool],
|
same_period: Callable[[float, float], bool],
|
||||||
@ -334,10 +325,9 @@ async def ws_get_fossil_energy_consumption(
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
merged_energy_statistics = _combine_sum_statistics(
|
merged_energy_statistics = _combine_change_statistics(
|
||||||
statistics, msg["energy_statistic_ids"]
|
statistics, msg["energy_statistic_ids"]
|
||||||
)
|
)
|
||||||
energy_deltas = _calculate_deltas(merged_energy_statistics)
|
|
||||||
indexed_co2_statistics = cast(
|
indexed_co2_statistics = cast(
|
||||||
dict[float, float],
|
dict[float, float],
|
||||||
{
|
{
|
||||||
@ -349,7 +339,7 @@ async def ws_get_fossil_energy_consumption(
|
|||||||
# Calculate amount of fossil based energy, assume 100% fossil if missing
|
# Calculate amount of fossil based energy, assume 100% fossil if missing
|
||||||
fossil_energy = [
|
fossil_energy = [
|
||||||
{"start": start, "delta": delta * indexed_co2_statistics.get(start, 100) / 100}
|
{"start": start, "delta": delta * indexed_co2_statistics.get(start, 100) / 100}
|
||||||
for start, delta in energy_deltas.items()
|
for start, delta in merged_energy_statistics.items()
|
||||||
]
|
]
|
||||||
|
|
||||||
if msg["period"] == "hour":
|
if msg["period"] == "hour":
|
||||||
|
@ -423,6 +423,7 @@ async def test_fossil_energy_consumption_no_co2(
|
|||||||
response = await client.receive_json()
|
response = await client.receive_json()
|
||||||
assert response["success"]
|
assert response["success"]
|
||||||
assert response["result"] == {
|
assert response["result"] == {
|
||||||
|
period1.isoformat(): pytest.approx(22.0),
|
||||||
period2.isoformat(): pytest.approx(33.0 - 22.0),
|
period2.isoformat(): pytest.approx(33.0 - 22.0),
|
||||||
period3.isoformat(): pytest.approx(55.0 - 33.0),
|
period3.isoformat(): pytest.approx(55.0 - 33.0),
|
||||||
period4.isoformat(): pytest.approx(88.0 - 55.0),
|
period4.isoformat(): pytest.approx(88.0 - 55.0),
|
||||||
@ -445,6 +446,7 @@ async def test_fossil_energy_consumption_no_co2(
|
|||||||
response = await client.receive_json()
|
response = await client.receive_json()
|
||||||
assert response["success"]
|
assert response["success"]
|
||||||
assert response["result"] == {
|
assert response["result"] == {
|
||||||
|
period1.isoformat(): pytest.approx(22.0),
|
||||||
period2_day_start.isoformat(): pytest.approx(33.0 - 22.0),
|
period2_day_start.isoformat(): pytest.approx(33.0 - 22.0),
|
||||||
period3.isoformat(): pytest.approx(55.0 - 33.0),
|
period3.isoformat(): pytest.approx(55.0 - 33.0),
|
||||||
period4_day_start.isoformat(): pytest.approx(88.0 - 55.0),
|
period4_day_start.isoformat(): pytest.approx(88.0 - 55.0),
|
||||||
@ -467,7 +469,7 @@ async def test_fossil_energy_consumption_no_co2(
|
|||||||
response = await client.receive_json()
|
response = await client.receive_json()
|
||||||
assert response["success"]
|
assert response["success"]
|
||||||
assert response["result"] == {
|
assert response["result"] == {
|
||||||
period1.isoformat(): pytest.approx(33.0 - 22.0),
|
period1.isoformat(): pytest.approx(33.0),
|
||||||
period3.isoformat(): pytest.approx((55.0 - 33.0) + (88.0 - 55.0)),
|
period3.isoformat(): pytest.approx((55.0 - 33.0) + (88.0 - 55.0)),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -586,8 +588,9 @@ async def test_fossil_energy_consumption_hole(
|
|||||||
response = await client.receive_json()
|
response = await client.receive_json()
|
||||||
assert response["success"]
|
assert response["success"]
|
||||||
assert response["result"] == {
|
assert response["result"] == {
|
||||||
period2.isoformat(): pytest.approx(3.0 - 20.0),
|
period1.isoformat(): pytest.approx(20.0),
|
||||||
period3.isoformat(): pytest.approx(55.0 - 3.0),
|
period2.isoformat(): pytest.approx(3.0),
|
||||||
|
period3.isoformat(): pytest.approx(32.0),
|
||||||
period4.isoformat(): pytest.approx(88.0 - 55.0),
|
period4.isoformat(): pytest.approx(88.0 - 55.0),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -608,8 +611,9 @@ async def test_fossil_energy_consumption_hole(
|
|||||||
response = await client.receive_json()
|
response = await client.receive_json()
|
||||||
assert response["success"]
|
assert response["success"]
|
||||||
assert response["result"] == {
|
assert response["result"] == {
|
||||||
period2_day_start.isoformat(): pytest.approx(3.0 - 20.0),
|
period1.isoformat(): pytest.approx(20.0),
|
||||||
period3.isoformat(): pytest.approx(55.0 - 3.0),
|
period2_day_start.isoformat(): pytest.approx(3.0),
|
||||||
|
period3.isoformat(): pytest.approx(32.0),
|
||||||
period4_day_start.isoformat(): pytest.approx(88.0 - 55.0),
|
period4_day_start.isoformat(): pytest.approx(88.0 - 55.0),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -630,8 +634,8 @@ async def test_fossil_energy_consumption_hole(
|
|||||||
response = await client.receive_json()
|
response = await client.receive_json()
|
||||||
assert response["success"]
|
assert response["success"]
|
||||||
assert response["result"] == {
|
assert response["result"] == {
|
||||||
period1.isoformat(): pytest.approx(3.0 - 20.0),
|
period1.isoformat(): pytest.approx(23.0),
|
||||||
period3.isoformat(): pytest.approx((55.0 - 3.0) + (88.0 - 55.0)),
|
period3.isoformat(): pytest.approx((55.0 - 3.0) + (88.0 - 55.0) - 20.0),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -930,6 +934,7 @@ async def test_fossil_energy_consumption(
|
|||||||
response = await client.receive_json()
|
response = await client.receive_json()
|
||||||
assert response["success"]
|
assert response["success"]
|
||||||
assert response["result"] == {
|
assert response["result"] == {
|
||||||
|
period1.isoformat(): pytest.approx(11.0 * 0.2),
|
||||||
period2.isoformat(): pytest.approx((33.0 - 22.0) * 0.3),
|
period2.isoformat(): pytest.approx((33.0 - 22.0) * 0.3),
|
||||||
period3.isoformat(): pytest.approx((44.0 - 33.0) * 0.6),
|
period3.isoformat(): pytest.approx((44.0 - 33.0) * 0.6),
|
||||||
period4.isoformat(): pytest.approx((55.0 - 44.0) * 0.9),
|
period4.isoformat(): pytest.approx((55.0 - 44.0) * 0.9),
|
||||||
@ -952,6 +957,7 @@ async def test_fossil_energy_consumption(
|
|||||||
response = await client.receive_json()
|
response = await client.receive_json()
|
||||||
assert response["success"]
|
assert response["success"]
|
||||||
assert response["result"] == {
|
assert response["result"] == {
|
||||||
|
period1.isoformat(): pytest.approx(11.0 * 0.2),
|
||||||
period2_day_start.isoformat(): pytest.approx((33.0 - 22.0) * 0.3),
|
period2_day_start.isoformat(): pytest.approx((33.0 - 22.0) * 0.3),
|
||||||
period3.isoformat(): pytest.approx((44.0 - 33.0) * 0.6),
|
period3.isoformat(): pytest.approx((44.0 - 33.0) * 0.6),
|
||||||
period4_day_start.isoformat(): pytest.approx((55.0 - 44.0) * 0.9),
|
period4_day_start.isoformat(): pytest.approx((55.0 - 44.0) * 0.9),
|
||||||
@ -974,7 +980,7 @@ async def test_fossil_energy_consumption(
|
|||||||
response = await client.receive_json()
|
response = await client.receive_json()
|
||||||
assert response["success"]
|
assert response["success"]
|
||||||
assert response["result"] == {
|
assert response["result"] == {
|
||||||
period1.isoformat(): pytest.approx((33.0 - 22.0) * 0.3),
|
period1.isoformat(): pytest.approx(11.0 * 0.5),
|
||||||
period3.isoformat(): pytest.approx(
|
period3.isoformat(): pytest.approx(
|
||||||
((44.0 - 33.0) * 0.6) + ((55.0 - 44.0) * 0.9)
|
((44.0 - 33.0) * 0.6) + ((55.0 - 44.0) * 0.9)
|
||||||
),
|
),
|
||||||
@ -1032,3 +1038,120 @@ async def test_fossil_energy_consumption_checks(
|
|||||||
assert msg["id"] == 2
|
assert msg["id"] == 2
|
||||||
assert not msg["success"]
|
assert not msg["success"]
|
||||||
assert msg["error"] == {"code": "invalid_end_time", "message": "Invalid end_time"}
|
assert msg["error"] == {"code": "invalid_end_time", "message": "Invalid end_time"}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.freeze_time("2021-08-01 01:00:00+00:00")
|
||||||
|
async def test_fossil_energy_consumption_check_missing_hour(
|
||||||
|
hass: HomeAssistant, hass_ws_client: WebSocketGenerator
|
||||||
|
) -> None:
|
||||||
|
"""Test explicitly if the API keeps the first hour of data for the requested time frame."""
|
||||||
|
|
||||||
|
now = dt_util.utcnow()
|
||||||
|
later = dt_util.as_utc(dt_util.parse_datetime("2021-08-01 05:00:00"))
|
||||||
|
|
||||||
|
await async_setup_component(hass, "history", {})
|
||||||
|
await async_setup_component(hass, "sensor", {})
|
||||||
|
await async_recorder_block_till_done(hass)
|
||||||
|
|
||||||
|
hour1 = dt_util.as_utc(dt_util.parse_datetime("2021-08-01 01:00:00"))
|
||||||
|
hour2 = dt_util.as_utc(dt_util.parse_datetime("2021-08-01 02:00:00"))
|
||||||
|
hour3 = dt_util.as_utc(dt_util.parse_datetime("2021-08-01 03:00:00"))
|
||||||
|
hour4 = dt_util.as_utc(dt_util.parse_datetime("2021-08-01 04:00:00"))
|
||||||
|
|
||||||
|
# add energy statistics for 4 hours
|
||||||
|
energy_statistics_1 = (
|
||||||
|
{
|
||||||
|
"start": hour1,
|
||||||
|
"last_reset": None,
|
||||||
|
"state": 0,
|
||||||
|
"sum": 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"start": hour2,
|
||||||
|
"last_reset": None,
|
||||||
|
"state": 1,
|
||||||
|
"sum": 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"start": hour3,
|
||||||
|
"last_reset": None,
|
||||||
|
"state": 2,
|
||||||
|
"sum": 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"start": hour4,
|
||||||
|
"last_reset": None,
|
||||||
|
"state": 3,
|
||||||
|
"sum": 8,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
energy_metadata_1 = {
|
||||||
|
"has_mean": False,
|
||||||
|
"has_sum": True,
|
||||||
|
"name": "Total imported energy",
|
||||||
|
"source": "test",
|
||||||
|
"statistic_id": "test:total_energy_import",
|
||||||
|
"unit_of_measurement": "kWh",
|
||||||
|
}
|
||||||
|
|
||||||
|
async_add_external_statistics(hass, energy_metadata_1, energy_statistics_1)
|
||||||
|
|
||||||
|
# add co2 statistics for 4 hours
|
||||||
|
co2_statistics = (
|
||||||
|
{
|
||||||
|
"start": hour1,
|
||||||
|
"last_reset": None,
|
||||||
|
"mean": 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"start": hour2,
|
||||||
|
"last_reset": None,
|
||||||
|
"mean": 30,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"start": hour3,
|
||||||
|
"last_reset": None,
|
||||||
|
"mean": 60,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"start": hour4,
|
||||||
|
"last_reset": None,
|
||||||
|
"mean": 90,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
co2_metadata = {
|
||||||
|
"has_mean": True,
|
||||||
|
"has_sum": False,
|
||||||
|
"name": "Fossil percentage",
|
||||||
|
"source": "test",
|
||||||
|
"statistic_id": "test:fossil_percentage",
|
||||||
|
"unit_of_measurement": "%",
|
||||||
|
}
|
||||||
|
|
||||||
|
async_add_external_statistics(hass, co2_metadata, co2_statistics)
|
||||||
|
await async_wait_recording_done(hass)
|
||||||
|
|
||||||
|
client = await hass_ws_client()
|
||||||
|
await client.send_json(
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": "energy/fossil_energy_consumption",
|
||||||
|
"start_time": now.isoformat(),
|
||||||
|
"end_time": later.isoformat(),
|
||||||
|
"energy_statistic_ids": [
|
||||||
|
"test:total_energy_import",
|
||||||
|
],
|
||||||
|
"co2_statistic_id": "test:fossil_percentage",
|
||||||
|
"period": "hour",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# check if we received deltas for the requested time frame
|
||||||
|
response = await client.receive_json()
|
||||||
|
assert response["success"]
|
||||||
|
assert list(response["result"].keys()) == [
|
||||||
|
hour1.isoformat(),
|
||||||
|
hour2.isoformat(),
|
||||||
|
hour3.isoformat(),
|
||||||
|
hour4.isoformat(),
|
||||||
|
]
|
||||||
|
Loading…
x
Reference in New Issue
Block a user