mirror of
https://github.com/home-assistant/core.git
synced 2026-10-06 14:29:21 -04:00
Improve handling of renamed energy power sensor (#183852)
This commit is contained in:
@@ -7,8 +7,14 @@ from typing import Any, Literal, NotRequired, TypedDict, override
|
||||
|
||||
import probatio
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback, valid_entity_id
|
||||
from homeassistant.helpers import config_validation as cv, singleton, storage
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import Event, HomeAssistant, callback, valid_entity_id
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
entity_registry as er,
|
||||
singleton,
|
||||
storage,
|
||||
)
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
@@ -766,6 +772,46 @@ class EnergyManager:
|
||||
async def async_initialize(self) -> None:
|
||||
"""Initialize the energy integration."""
|
||||
self.data = await self._store.async_load()
|
||||
# The power sensors may have been renamed while energy was not loaded
|
||||
self._async_refresh_power_stat_rates()
|
||||
self._hass.bus.async_listen(
|
||||
er.EVENT_ENTITY_REGISTRY_UPDATED,
|
||||
self._async_entity_registry_updated,
|
||||
event_filter=self._async_filter_power_sensor_registry_updated,
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_filter_power_sensor_registry_updated(
|
||||
self, event_data: er.EventEntityRegistryUpdatedData
|
||||
) -> bool:
|
||||
"""Filter for power sensors being created or getting a new entity_id."""
|
||||
if event_data["action"] == "update":
|
||||
if "entity_id" not in event_data["changes"]:
|
||||
return False
|
||||
elif event_data["action"] != "create":
|
||||
return False
|
||||
entry = er.async_get(self._hass).async_get(event_data["entity_id"])
|
||||
return entry is not None and entry.platform == DOMAIN
|
||||
|
||||
@callback
|
||||
def _async_entity_registry_updated(
|
||||
self, event: Event[er.EventEntityRegistryUpdatedData]
|
||||
) -> None:
|
||||
"""Handle a power sensor being created or getting a new entity_id."""
|
||||
self._async_refresh_power_stat_rates()
|
||||
|
||||
@callback
|
||||
def _async_refresh_power_stat_rates(self) -> None:
|
||||
"""Point stat_rate of power transform configs at the actual entity_id."""
|
||||
if self.data is None:
|
||||
return
|
||||
energy_sources = self._process_energy_sources(self.data["energy_sources"])
|
||||
if energy_sources == self.data["energy_sources"]:
|
||||
return
|
||||
data = self.data.copy()
|
||||
data["energy_sources"] = energy_sources
|
||||
self.data = data
|
||||
self._store.async_delay_save(lambda: data, 60)
|
||||
|
||||
@staticmethod
|
||||
def default_preferences() -> EnergyPreferences:
|
||||
@@ -805,20 +851,34 @@ class EnergyManager:
|
||||
|
||||
await asyncio.gather(*(listener() for listener in self._update_listeners))
|
||||
|
||||
def _power_sensor_entity_id(self, source_type: str, config: PowerConfig) -> str:
|
||||
"""Return the entity_id of a power transform sensor.
|
||||
|
||||
The generated entity_id is only a suggestion, the sensor may have been
|
||||
renamed or registered with another entity_id.
|
||||
"""
|
||||
from .helpers import ( # noqa: PLC0415
|
||||
generate_power_sensor_entity_id,
|
||||
generate_power_sensor_unique_id,
|
||||
)
|
||||
|
||||
unique_id = generate_power_sensor_unique_id(source_type, config)
|
||||
if entity_id := er.async_get(self._hass).async_get_entity_id(
|
||||
Platform.SENSOR, DOMAIN, unique_id
|
||||
):
|
||||
return entity_id
|
||||
return generate_power_sensor_entity_id(source_type, config)
|
||||
|
||||
def _process_energy_sources(self, sources: list[SourceType]) -> list[SourceType]:
|
||||
"""Process energy sources and set stat_rate for power configs."""
|
||||
from .helpers import generate_power_sensor_entity_id # noqa: PLC0415
|
||||
|
||||
processed: list[SourceType] = []
|
||||
for source in sources:
|
||||
if source["type"] == "battery":
|
||||
source = self._process_battery_power(
|
||||
source, generate_power_sensor_entity_id
|
||||
source, self._power_sensor_entity_id
|
||||
)
|
||||
elif source["type"] == "grid":
|
||||
source = self._process_grid_power(
|
||||
source, generate_power_sensor_entity_id
|
||||
)
|
||||
source = self._process_grid_power(source, self._power_sensor_entity_id)
|
||||
processed.append(source)
|
||||
return processed
|
||||
|
||||
@@ -837,7 +897,7 @@ class EnergyManager:
|
||||
if "stat_rate" in config:
|
||||
return {**source, "stat_rate": config["stat_rate"]}
|
||||
|
||||
# For inverted or two-sensor config, set stat_rate to the generated entity_id
|
||||
# For inverted or two-sensor config, set stat_rate to the power sensor
|
||||
return {**source, "stat_rate": generate_entity_id("battery", config)}
|
||||
|
||||
def _process_grid_power(
|
||||
@@ -855,7 +915,7 @@ class EnergyManager:
|
||||
if "stat_rate" in config:
|
||||
return {**source, "stat_rate": config["stat_rate"]}
|
||||
|
||||
# For inverted or two-sensor config, set stat_rate to the generated entity_id
|
||||
# For inverted or two-sensor config, set stat_rate to the power sensor
|
||||
return {**source, "stat_rate": generate_entity_id("grid", config)}
|
||||
|
||||
@callback
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Test energy data storage and migration."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import probatio
|
||||
import pytest
|
||||
|
||||
@@ -13,7 +15,7 @@ from homeassistant.components.energy.data import (
|
||||
EnergyManager,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import storage
|
||||
from homeassistant.helpers import entity_registry as er, storage
|
||||
|
||||
|
||||
async def test_energy_preferences_no_migration_needed(hass: HomeAssistant) -> None:
|
||||
@@ -42,6 +44,44 @@ async def test_energy_preferences_no_migration_needed(hass: HomeAssistant) -> No
|
||||
]
|
||||
|
||||
|
||||
async def test_energy_preferences_load_resolves_renamed_power_sensor(
|
||||
hass: HomeAssistant,
|
||||
hass_storage: dict[str, Any],
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test a stale stat_rate is pointed at the renamed power sensor on load."""
|
||||
entity_registry.async_get_or_create(
|
||||
"sensor",
|
||||
"energy",
|
||||
"energy_power_battery_inverted_sensor_battery_power",
|
||||
suggested_object_id="renamed_power",
|
||||
)
|
||||
hass_storage["energy"] = {
|
||||
"version": 1,
|
||||
"minor_version": 3,
|
||||
"key": "energy",
|
||||
"data": {
|
||||
"energy_sources": [
|
||||
{
|
||||
"type": "battery",
|
||||
"stat_energy_from": "sensor.battery_energy_from",
|
||||
"stat_energy_to": "sensor.battery_energy_to",
|
||||
"power_config": {"stat_rate_inverted": "sensor.battery_power"},
|
||||
"stat_rate": "sensor.battery_power_inverted",
|
||||
}
|
||||
],
|
||||
"device_consumption": [],
|
||||
"device_consumption_water": [],
|
||||
},
|
||||
}
|
||||
|
||||
manager = EnergyManager(hass)
|
||||
await manager.async_initialize()
|
||||
|
||||
assert manager.data is not None
|
||||
assert manager.data["energy_sources"][0]["stat_rate"] == "sensor.renamed_power"
|
||||
|
||||
|
||||
async def test_energy_preferences_default(hass: HomeAssistant) -> None:
|
||||
"""Test default preferences include device_consumption_water."""
|
||||
defaults = EnergyManager.default_preferences()
|
||||
@@ -727,11 +767,10 @@ async def test_grid_migration_more_imports_than_exports(hass: HomeAssistant) ->
|
||||
|
||||
|
||||
async def test_grid_migration_with_power(hass: HomeAssistant) -> None:
|
||||
"""Test migration preserves power config and stat_rate from first grid.
|
||||
"""Test migration preserves power config from first grid.
|
||||
|
||||
Note: Migration preserves the original stat_rate value from the legacy power array.
|
||||
The stat_rate regeneration from power_config only happens during async_update()
|
||||
for new data submissions, not during storage migration.
|
||||
Note: stat_rate is regenerated from power_config when the preferences are
|
||||
loaded, so a stale stat_rate from the legacy power array is replaced.
|
||||
"""
|
||||
old_data = {
|
||||
"energy_sources": [
|
||||
@@ -768,9 +807,7 @@ async def test_grid_migration_with_power(hass: HomeAssistant) -> None:
|
||||
# Verify power_config is preserved
|
||||
assert grid["power_config"] == {"stat_rate_inverted": "sensor.grid_power"}
|
||||
|
||||
# Migration preserves the original stat_rate value from the legacy power array
|
||||
# (stat_rate regeneration from power_config only happens in async_update)
|
||||
assert grid["stat_rate"] == "sensor.grid_power"
|
||||
assert grid["stat_rate"] == "sensor.grid_power_inverted"
|
||||
|
||||
|
||||
async def test_grid_migration_import_only(hass: HomeAssistant) -> None:
|
||||
|
||||
@@ -41,7 +41,7 @@ from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.unit_conversion import _WH_TO_CAL, _WH_TO_J
|
||||
from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
from tests.components.recorder.common import async_wait_recording_done
|
||||
from tests.typing import WebSocketGenerator
|
||||
|
||||
@@ -2807,3 +2807,143 @@ async def test_add_finished_resolved_on_abort(
|
||||
sensor.add_to_platform_abort()
|
||||
|
||||
assert sensor.add_finished.done()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("energy_source", "power_entity_id"),
|
||||
[
|
||||
pytest.param(
|
||||
{
|
||||
"type": "battery",
|
||||
"stat_energy_from": "sensor.battery_energy_from",
|
||||
"stat_energy_to": "sensor.battery_energy_to",
|
||||
"power_config": {"stat_rate_inverted": "sensor.battery_power"},
|
||||
},
|
||||
"sensor.battery_power_inverted",
|
||||
id="battery_inverted",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"type": "grid",
|
||||
"stat_energy_from": "sensor.grid_energy_import",
|
||||
"stat_energy_to": "sensor.grid_energy_export",
|
||||
"power_config": {
|
||||
"stat_rate_from": "sensor.grid_import",
|
||||
"stat_rate_to": "sensor.grid_export",
|
||||
},
|
||||
"cost_adjustment_day": 0,
|
||||
},
|
||||
"sensor.energy_grid_grid_import_grid_export_net_power",
|
||||
id="grid_combined",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_power_sensor_rename_updates_stat_rate(
|
||||
recorder_mock: Recorder,
|
||||
hass: HomeAssistant,
|
||||
hass_storage: dict[str, Any],
|
||||
entity_registry: er.EntityRegistry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
energy_source: data.SourceType,
|
||||
power_entity_id: str,
|
||||
) -> None:
|
||||
"""Test renaming a power sensor updates stat_rate in the preferences."""
|
||||
assert await async_setup_component(hass, DOMAIN, {"energy": {}})
|
||||
manager = await async_get_manager(hass)
|
||||
manager.data = manager.default_preferences()
|
||||
|
||||
await manager.async_update({"energy_sources": [energy_source]})
|
||||
await hass.async_block_till_done()
|
||||
assert manager.data["energy_sources"][0]["stat_rate"] == power_entity_id
|
||||
|
||||
entity_registry.async_update_entity(
|
||||
power_entity_id, new_entity_id="sensor.renamed_power"
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get("sensor.renamed_power") is not None
|
||||
assert manager.data["energy_sources"][0]["stat_rate"] == "sensor.renamed_power"
|
||||
|
||||
freezer.tick(timedelta(seconds=60))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
assert (
|
||||
hass_storage[data.STORAGE_KEY]["data"]["energy_sources"][0]["stat_rate"]
|
||||
== "sensor.renamed_power"
|
||||
)
|
||||
|
||||
|
||||
async def test_power_sensor_rename_other_entity_keeps_prefs(
|
||||
recorder_mock: Recorder,
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test renaming an entity which is not a power sensor leaves prefs alone."""
|
||||
assert await async_setup_component(hass, DOMAIN, {"energy": {}})
|
||||
manager = await async_get_manager(hass)
|
||||
manager.data = manager.default_preferences()
|
||||
entity_registry.async_get_or_create(
|
||||
"sensor", "test", "battery_power", suggested_object_id="battery_power"
|
||||
)
|
||||
|
||||
await manager.async_update(
|
||||
{
|
||||
"energy_sources": [
|
||||
{
|
||||
"type": "battery",
|
||||
"stat_energy_from": "sensor.battery_energy_from",
|
||||
"stat_energy_to": "sensor.battery_energy_to",
|
||||
"power_config": {"stat_rate_inverted": "sensor.battery_power"},
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
prefs = manager.data
|
||||
|
||||
entity_registry.async_update_entity(
|
||||
"sensor.battery_power", new_entity_id="sensor.battery_power_renamed"
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert manager.data is prefs
|
||||
assert prefs["energy_sources"][0]["stat_rate"] == "sensor.battery_power_inverted"
|
||||
|
||||
|
||||
async def test_power_sensor_suggested_entity_id_taken(
|
||||
recorder_mock: Recorder,
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test stat_rate follows the power sensor when its entity_id is taken."""
|
||||
assert await async_setup_component(hass, DOMAIN, {"energy": {}})
|
||||
manager = await async_get_manager(hass)
|
||||
manager.data = manager.default_preferences()
|
||||
entity_registry.async_get_or_create(
|
||||
"sensor", "test", "taken", suggested_object_id="battery_power_inverted"
|
||||
)
|
||||
|
||||
await manager.async_update(
|
||||
{
|
||||
"energy_sources": [
|
||||
{
|
||||
"type": "battery",
|
||||
"stat_energy_from": "sensor.battery_energy_from",
|
||||
"stat_energy_to": "sensor.battery_energy_to",
|
||||
"power_config": {"stat_rate_inverted": "sensor.battery_power"},
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (
|
||||
entity_registry.async_get_entity_id(
|
||||
"sensor", DOMAIN, "energy_power_battery_inverted_sensor_battery_power"
|
||||
)
|
||||
== "sensor.battery_power_inverted_2"
|
||||
)
|
||||
assert (
|
||||
manager.data["energy_sources"][0]["stat_rate"]
|
||||
== "sensor.battery_power_inverted_2"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user