Skip to content

Commit 43e4fe4

Browse files
authored
Add support for downstream water meters in energy dashboard (home-assistant#155927)
1 parent 252dbb7 commit 43e4fe4

File tree

7 files changed

+294
-3
lines changed

7 files changed

+294
-3
lines changed

homeassistant/components/energy/data.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import asyncio
66
from collections import Counter
77
from collections.abc import Awaitable, Callable
8-
from typing import Literal, NotRequired, TypedDict
8+
from typing import Any, Literal, NotRequired, TypedDict
99

1010
import voluptuous as vol
1111

@@ -15,6 +15,7 @@
1515
from .const import DOMAIN
1616

1717
STORAGE_VERSION = 1
18+
STORAGE_MINOR_VERSION = 2
1819
STORAGE_KEY = DOMAIN
1920

2021

@@ -164,6 +165,7 @@ class EnergyPreferences(TypedDict):
164165

165166
energy_sources: list[SourceType]
166167
device_consumption: list[DeviceConsumption]
168+
device_consumption_water: NotRequired[list[DeviceConsumption]]
167169

168170

169171
class EnergyPreferencesUpdate(EnergyPreferences, total=False):
@@ -328,14 +330,31 @@ def check_type_limits(value: list[SourceType]) -> list[SourceType]:
328330
)
329331

330332

333+
class _EnergyPreferencesStore(storage.Store[EnergyPreferences]):
334+
"""Energy preferences store with migration support."""
335+
336+
async def _async_migrate_func(
337+
self,
338+
old_major_version: int,
339+
old_minor_version: int,
340+
old_data: dict[str, Any],
341+
) -> dict[str, Any]:
342+
"""Migrate to the new version."""
343+
data = old_data
344+
if old_major_version == 1 and old_minor_version < 2:
345+
# Add device_consumption_water field if it doesn't exist
346+
data.setdefault("device_consumption_water", [])
347+
return data
348+
349+
331350
class EnergyManager:
332351
"""Manage the instance energy prefs."""
333352

334353
def __init__(self, hass: HomeAssistant) -> None:
335354
"""Initialize energy manager."""
336355
self._hass = hass
337-
self._store = storage.Store[EnergyPreferences](
338-
hass, STORAGE_VERSION, STORAGE_KEY
356+
self._store = _EnergyPreferencesStore(
357+
hass, STORAGE_VERSION, STORAGE_KEY, minor_version=STORAGE_MINOR_VERSION
339358
)
340359
self.data: EnergyPreferences | None = None
341360
self._update_listeners: list[Callable[[], Awaitable]] = []
@@ -350,6 +369,7 @@ def default_preferences() -> EnergyPreferences:
350369
return {
351370
"energy_sources": [],
352371
"device_consumption": [],
372+
"device_consumption_water": [],
353373
}
354374

355375
async def async_update(self, update: EnergyPreferencesUpdate) -> None:
@@ -362,6 +382,7 @@ async def async_update(self, update: EnergyPreferencesUpdate) -> None:
362382
for key in (
363383
"energy_sources",
364384
"device_consumption",
385+
"device_consumption_water",
365386
):
366387
if key in update:
367388
data[key] = update[key]

homeassistant/components/energy/validate.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,9 @@ class EnergyPreferencesValidation:
153153

154154
energy_sources: list[ValidationIssues] = dataclasses.field(default_factory=list)
155155
device_consumption: list[ValidationIssues] = dataclasses.field(default_factory=list)
156+
device_consumption_water: list[ValidationIssues] = dataclasses.field(
157+
default_factory=list
158+
)
156159

157160
def as_dict(self) -> dict:
158161
"""Return dictionary version."""
@@ -165,6 +168,10 @@ def as_dict(self) -> dict:
165168
[dataclasses.asdict(issue) for issue in issues.issues.values()]
166169
for issues in self.device_consumption
167170
],
171+
"device_consumption_water": [
172+
[dataclasses.asdict(issue) for issue in issues.issues.values()]
173+
for issues in self.device_consumption_water
174+
],
168175
}
169176

170177

@@ -742,6 +749,23 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation:
742749
)
743750
)
744751

752+
for device in manager.data.get("device_consumption_water", []):
753+
device_result = ValidationIssues()
754+
result.device_consumption_water.append(device_result)
755+
wanted_statistics_metadata.add(device["stat_consumption"])
756+
validate_calls.append(
757+
functools.partial(
758+
_async_validate_usage_stat,
759+
hass,
760+
statistics_metadata,
761+
device["stat_consumption"],
762+
WATER_USAGE_DEVICE_CLASSES,
763+
WATER_USAGE_UNITS,
764+
WATER_UNIT_ERROR,
765+
device_result,
766+
)
767+
)
768+
745769
# Fetch the needed statistics metadata
746770
statistics_metadata.update(
747771
await recorder.get_instance(hass).async_add_executor_job(

homeassistant/components/energy/websocket_api.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ def ws_get_prefs(
129129
vol.Required("type"): "energy/save_prefs",
130130
vol.Optional("energy_sources"): ENERGY_SOURCE_SCHEMA,
131131
vol.Optional("device_consumption"): [DEVICE_CONSUMPTION_SCHEMA],
132+
vol.Optional("device_consumption_water"): [DEVICE_CONSUMPTION_SCHEMA],
132133
}
133134
)
134135
@websocket_api.async_response
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""Test energy data storage and migration."""
2+
3+
from homeassistant.components.energy.data import EnergyManager
4+
from homeassistant.core import HomeAssistant
5+
from homeassistant.helpers import storage
6+
7+
8+
async def test_energy_preferences_no_migration_needed(hass: HomeAssistant) -> None:
9+
"""Test that new data format doesn't get migrated."""
10+
# Create new format data (already has device_consumption_water field)
11+
new_data = {
12+
"energy_sources": [],
13+
"device_consumption": [],
14+
"device_consumption_water": [
15+
{"stat_consumption": "sensor.water_meter", "name": "Water heater"}
16+
],
17+
}
18+
19+
# Save data that already has the new field
20+
old_store = storage.Store(hass, 1, "energy", minor_version=1)
21+
await old_store.async_save(new_data)
22+
23+
# Load it with manager
24+
manager = EnergyManager(hass)
25+
await manager.async_initialize()
26+
27+
# Verify the data is unchanged
28+
assert manager.data is not None
29+
assert manager.data["device_consumption_water"] == [
30+
{"stat_consumption": "sensor.water_meter", "name": "Water heater"}
31+
]
32+
33+
34+
async def test_energy_preferences_default(hass: HomeAssistant) -> None:
35+
"""Test default preferences include device_consumption_water."""
36+
defaults = EnergyManager.default_preferences()
37+
38+
assert "energy_sources" in defaults
39+
assert "device_consumption" in defaults
40+
assert "device_consumption_water" in defaults
41+
assert defaults["device_consumption_water"] == []
42+
43+
44+
async def test_energy_preferences_empty_store(hass: HomeAssistant) -> None:
45+
"""Test loading with no existing data."""
46+
manager = EnergyManager(hass)
47+
await manager.async_initialize()
48+
49+
# Verify data is None when no existing data
50+
assert manager.data is None
51+
52+
53+
async def test_energy_preferences_migration_from_old_version(
54+
hass: HomeAssistant,
55+
) -> None:
56+
"""Test that device_consumption_water is added when migrating from v1.1 to v1.2."""
57+
# Create version 1.1 data without device_consumption_water (old version)
58+
old_data = {
59+
"energy_sources": [],
60+
"device_consumption": [],
61+
}
62+
63+
# Save with old version (1.1) - migration will run to upgrade to 1.2
64+
old_store = storage.Store(hass, 1, "energy", minor_version=1)
65+
await old_store.async_save(old_data)
66+
67+
# Load with manager - should trigger migration
68+
manager = EnergyManager(hass)
69+
await manager.async_initialize()
70+
71+
# Verify the field was added by migration
72+
assert manager.data is not None
73+
assert "device_consumption_water" in manager.data
74+
assert manager.data["device_consumption_water"] == []

0 commit comments

Comments
 (0)