mirror of
https://github.com/home-assistant/core.git
synced 2026-09-10 07:31:41 +01:00
Bump solaredge-web to 0.3.0 (#177899)
This commit is contained in:
@@ -6,7 +6,7 @@ from datetime import datetime, timedelta
|
||||
from typing import TYPE_CHECKING, Any, override
|
||||
|
||||
from aiosolaredge import SolarEdge
|
||||
from solaredge_web import EnergyData, SolarEdgeWeb, TimeUnit
|
||||
from solaredge_web import EnergyData, SolarEdgeWeb
|
||||
|
||||
from homeassistant.components.recorder import get_instance
|
||||
from homeassistant.components.recorder.models import (
|
||||
@@ -16,6 +16,7 @@ from homeassistant.components.recorder.models import (
|
||||
)
|
||||
from homeassistant.components.recorder.statistics import (
|
||||
async_add_external_statistics,
|
||||
async_list_statistic_ids,
|
||||
get_last_statistics,
|
||||
statistics_during_period,
|
||||
)
|
||||
@@ -23,7 +24,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, UnitOfEnergy
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util import dt as dt_util, snakecase
|
||||
from homeassistant.util import dt as dt_util, slugify, snakecase
|
||||
from homeassistant.util.unit_conversion import EnergyConverter
|
||||
|
||||
from .const import (
|
||||
@@ -455,6 +456,8 @@ class SolarEdgeModulesCoordinator(DataUpdateCoordinator[None]):
|
||||
)
|
||||
self.site_id = config_entry.data[CONF_SITE_ID]
|
||||
self.title = config_entry.title
|
||||
self._serial_to_legacy_id: dict[str, str] = {}
|
||||
self._legacy_id_map_initialized = False
|
||||
|
||||
@callback
|
||||
def _dummy_listener() -> None:
|
||||
@@ -467,13 +470,12 @@ class SolarEdgeModulesCoordinator(DataUpdateCoordinator[None]):
|
||||
@override
|
||||
async def _async_update_data(self) -> None:
|
||||
"""Fetch data from API endpoint and update statistics."""
|
||||
equipment: dict[int, dict[str, Any]] = await self.api.async_get_equipment()
|
||||
equipment: dict[str, dict[str, Any]] = await self.api.async_get_equipment()
|
||||
await self._async_build_legacy_id_map(equipment)
|
||||
# We fetch last week's data from the API and refresh
|
||||
# every 12h so we overwrite recent statistics. This is
|
||||
# intended to allow adding any corrected/updated data.
|
||||
energy_data_list: list[EnergyData] = await self.api.async_get_energy_data(
|
||||
TimeUnit.WEEK
|
||||
)
|
||||
energy_data_list: list[EnergyData] = await self.api.async_get_energy_data()
|
||||
if not energy_data_list:
|
||||
LOGGER.warning(
|
||||
"No data received from SolarEdge API for site: %s", self.site_id
|
||||
@@ -486,9 +488,7 @@ class SolarEdgeModulesCoordinator(DataUpdateCoordinator[None]):
|
||||
),
|
||||
)
|
||||
for equipment_id, equipment_data in equipment.items():
|
||||
display_name = equipment_data.get(
|
||||
"displayName", f"Equipment {equipment_id}"
|
||||
)
|
||||
display_name = equipment_data.get("name", f"Equipment {equipment_id}")
|
||||
statistic_id = self.get_statistic_id(equipment_id)
|
||||
statistic_metadata = StatisticMetaData(
|
||||
mean_type=StatisticMeanType.ARITHMETIC,
|
||||
@@ -501,30 +501,19 @@ class SolarEdgeModulesCoordinator(DataUpdateCoordinator[None]):
|
||||
)
|
||||
statistic_sum = last_sums[statistic_id]
|
||||
statistics = []
|
||||
current_hour_sum = 0.0
|
||||
current_hour_count = 0
|
||||
for energy_data in energy_data_list:
|
||||
start_time = energy_data.start_time.replace(
|
||||
tzinfo=dt_util.get_default_time_zone()
|
||||
)
|
||||
value = energy_data.values.get(equipment_id, 0.0)
|
||||
current_hour_sum += value
|
||||
current_hour_count += 1
|
||||
if start_time.minute != 45:
|
||||
continue
|
||||
# API returns data every 15 minutes; aggregate to 1-hour statistics
|
||||
# when we reach the energy_data for the last 15 minutes of the hour.
|
||||
current_avg = current_hour_sum / current_hour_count
|
||||
statistic_sum += current_avg
|
||||
statistic_sum += value
|
||||
statistics.append(
|
||||
StatisticData(
|
||||
start=start_time - timedelta(minutes=45),
|
||||
state=current_avg,
|
||||
start=start_time,
|
||||
state=value,
|
||||
sum=statistic_sum,
|
||||
)
|
||||
)
|
||||
current_hour_sum = 0.0
|
||||
current_hour_count = 0
|
||||
LOGGER.debug(
|
||||
"Adding %s statistics for %s %s",
|
||||
len(statistics),
|
||||
@@ -533,12 +522,90 @@ class SolarEdgeModulesCoordinator(DataUpdateCoordinator[None]):
|
||||
)
|
||||
async_add_external_statistics(self.hass, statistic_metadata, statistics)
|
||||
|
||||
def get_statistic_id(self, equipment_id: int) -> str:
|
||||
"""Return the statistic ID for this equipment_id."""
|
||||
async def _async_build_legacy_id_map(
|
||||
self, equipment: dict[str, dict[str, Any]]
|
||||
) -> None:
|
||||
"""Build a map from serial numbers to legacy numeric statistic IDs.
|
||||
|
||||
The old API returned numeric equipment IDs; the new API returns serials.
|
||||
To preserve existing statistics, we map each serial's name (the last
|
||||
part after splitting by space, e.g. "1.1.1") to the old statistic ID
|
||||
by matching the statistic name. This runs once per coordinator lifecycle.
|
||||
"""
|
||||
if self._legacy_id_map_initialized:
|
||||
return
|
||||
|
||||
all_stats = await async_list_statistic_ids(self.hass)
|
||||
|
||||
prefix = f"{DOMAIN}:{self.site_id}_"
|
||||
# Map the short name (e.g. "1.1.1") to the old numeric statistic ID.
|
||||
# Multiple numeric IDs may share the same name, e.g. after a module
|
||||
# replacement; those matches are ambiguous and skipped.
|
||||
name_to_legacy: dict[str, str] = {}
|
||||
ambiguous_names: set[str] = set()
|
||||
for stat in all_stats:
|
||||
stat_id = stat["statistic_id"]
|
||||
if not stat_id.startswith(prefix):
|
||||
continue
|
||||
suffix = stat_id[len(prefix) :]
|
||||
if not suffix.isdigit():
|
||||
continue
|
||||
name = stat.get("name", "")
|
||||
# Use the last part after splitting by space (e.g. "solaredge 1.1.1" -> "1.1.1").
|
||||
short_name = name.rsplit(" ", 1)[-1] if name else ""
|
||||
if not short_name:
|
||||
continue
|
||||
if short_name in name_to_legacy:
|
||||
del name_to_legacy[short_name]
|
||||
ambiguous_names.add(short_name)
|
||||
elif short_name not in ambiguous_names:
|
||||
name_to_legacy[short_name] = stat_id
|
||||
|
||||
self._legacy_id_map_initialized = True
|
||||
|
||||
if not name_to_legacy and not ambiguous_names:
|
||||
return
|
||||
|
||||
# Map each serial to its old statistic ID via the equipment name.
|
||||
for serial, data in equipment.items():
|
||||
name = data.get("name", "")
|
||||
short_name = name.rsplit(" ", 1)[-1] if name else ""
|
||||
if not short_name:
|
||||
continue
|
||||
if short_name in ambiguous_names:
|
||||
LOGGER.warning(
|
||||
"Skipping legacy statistics migration for %s %s: multiple "
|
||||
"numeric statistics share the name %s, so the match is "
|
||||
"ambiguous",
|
||||
self.site_id,
|
||||
name,
|
||||
short_name,
|
||||
)
|
||||
continue
|
||||
if short_name in name_to_legacy:
|
||||
self._serial_to_legacy_id[serial] = name_to_legacy[short_name]
|
||||
|
||||
if self._serial_to_legacy_id:
|
||||
LOGGER.debug(
|
||||
"Mapped %s legacy SolarEdge statistics to serials for site %s",
|
||||
len(self._serial_to_legacy_id),
|
||||
self.site_id,
|
||||
)
|
||||
|
||||
def get_statistic_id(self, equipment_id: int | str) -> str:
|
||||
"""Return the statistic ID for this equipment_id.
|
||||
|
||||
If a legacy numeric ID exists for this serial, reuse it for backward
|
||||
compatibility. Otherwise, create a new serial-based ID.
|
||||
"""
|
||||
if isinstance(equipment_id, str) and equipment_id in self._serial_to_legacy_id:
|
||||
return self._serial_to_legacy_id[equipment_id]
|
||||
if isinstance(equipment_id, str):
|
||||
equipment_id = slugify(equipment_id)
|
||||
return f"{DOMAIN}:{self.site_id}_{equipment_id}"
|
||||
|
||||
async def _async_get_last_sums(
|
||||
self, equipment_ids: Iterable[int], start_time: datetime
|
||||
self, equipment_ids: Iterable[int | str], start_time: datetime
|
||||
) -> dict[str, float]:
|
||||
"""Get the last sum from the recorder before start_time for each statistic."""
|
||||
start = start_time - timedelta(hours=1)
|
||||
|
||||
@@ -14,5 +14,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aiosolaredge", "solaredge_web"],
|
||||
"requirements": ["aiosolaredge==1.0.2", "solaredge-web==0.0.1"]
|
||||
"requirements": ["aiosolaredge==1.0.2", "solaredge-web==0.3.0"]
|
||||
}
|
||||
|
||||
Generated
+1
-1
@@ -3068,7 +3068,7 @@ soco==0.31.1
|
||||
solaredge-local==0.2.3
|
||||
|
||||
# homeassistant.components.solaredge
|
||||
solaredge-web==0.0.1
|
||||
solaredge-web==0.3.0
|
||||
|
||||
# homeassistant.components.solarlog
|
||||
solarlog_cli==0.7.1
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
"""Common fixtures for the SolarEdge tests."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from datetime import datetime
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
from solaredge_web import EnergyData
|
||||
|
||||
from homeassistant.components.solaredge.const import CONF_SITE_ID, DOMAIN
|
||||
from homeassistant.const import CONF_API_KEY, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from tests.common import MockConfigEntry, load_json_object_fixture
|
||||
|
||||
@@ -102,7 +105,13 @@ def mock_solaredge_web_api_fixture() -> Generator[AsyncMock]:
|
||||
api = mock_web_api_flow.return_value
|
||||
mock_web_api_coord.return_value = api
|
||||
api.async_get_equipment.return_value = {
|
||||
1001: {"displayName": "1.1"},
|
||||
1002: {"displayName": "1.2"},
|
||||
"7A012345-CA": {"name": "Optimizer 1.1"},
|
||||
"7A012346-CA": {"name": "Optimizer 1.2"},
|
||||
}
|
||||
api.async_get_energy_data.return_value = [
|
||||
EnergyData(
|
||||
start_time=dt_util.as_utc(datetime(2025, 1, 1, 10, 0)),
|
||||
values={"7A012345-CA": 10.0, "7A012346-CA": 20.0},
|
||||
),
|
||||
]
|
||||
yield api
|
||||
|
||||
@@ -10,7 +10,15 @@ import pytest
|
||||
from solaredge_web import EnergyData
|
||||
|
||||
from homeassistant.components.recorder import Recorder
|
||||
from homeassistant.components.recorder.statistics import statistics_during_period
|
||||
from homeassistant.components.recorder.models import (
|
||||
StatisticMeanType,
|
||||
StatisticMetaData,
|
||||
)
|
||||
from homeassistant.components.recorder.statistics import (
|
||||
async_add_external_statistics,
|
||||
async_list_statistic_ids,
|
||||
statistics_during_period,
|
||||
)
|
||||
from homeassistant.components.solaredge.const import (
|
||||
CONF_SITE_ID,
|
||||
DATA_MODULES_COORDINATOR,
|
||||
@@ -29,6 +37,7 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.unit_conversion import EnergyConverter
|
||||
|
||||
from . import setup_integration
|
||||
from .conftest import API_KEY, PASSWORD, SITE_ID, USERNAME
|
||||
@@ -169,41 +178,17 @@ def mock_solar_edge_web() -> AsyncMock:
|
||||
) as mock_api:
|
||||
api = mock_api.return_value
|
||||
api.async_get_equipment.return_value = {
|
||||
1001: {"displayName": "1.1"},
|
||||
1002: {"displayName": "1.2"},
|
||||
"7A012345-CA": {"name": "Optimizer 1.1"},
|
||||
"7A012346-CA": {"name": "Optimizer 1.2"},
|
||||
}
|
||||
api.async_get_energy_data.return_value = [
|
||||
EnergyData(
|
||||
start_time=dt_util.as_utc(datetime(2025, 1, 1, 10, 0)),
|
||||
values={1001: 10.0, 1002: 20.0},
|
||||
),
|
||||
EnergyData(
|
||||
start_time=dt_util.as_utc(datetime(2025, 1, 1, 10, 15)),
|
||||
values={1001: 11.0, 1002: 21.0},
|
||||
),
|
||||
EnergyData(
|
||||
start_time=dt_util.as_utc(datetime(2025, 1, 1, 10, 30)),
|
||||
values={1001: 12.0, 1002: 22.0},
|
||||
),
|
||||
EnergyData(
|
||||
start_time=dt_util.as_utc(datetime(2025, 1, 1, 10, 45)),
|
||||
values={1001: 13.0, 1002: 23.0},
|
||||
values={"7A012345-CA": 10.0, "7A012346-CA": 20.0},
|
||||
),
|
||||
EnergyData(
|
||||
start_time=dt_util.as_utc(datetime(2025, 1, 1, 11, 0)),
|
||||
values={1001: 14.0, 1002: 24.0},
|
||||
),
|
||||
EnergyData(
|
||||
start_time=dt_util.as_utc(datetime(2025, 1, 1, 11, 15)),
|
||||
values={1001: 15.0, 1002: 25.0},
|
||||
),
|
||||
EnergyData(
|
||||
start_time=dt_util.as_utc(datetime(2025, 1, 1, 11, 30)),
|
||||
values={1001: 16.0, 1002: 26.0},
|
||||
),
|
||||
EnergyData(
|
||||
start_time=dt_util.as_utc(datetime(2025, 1, 1, 11, 45)),
|
||||
values={1001: 17.0, 1002: 27.0},
|
||||
values={"7A012345-CA": 14.0, "7A012346-CA": 24.0},
|
||||
),
|
||||
]
|
||||
yield api
|
||||
@@ -229,19 +214,19 @@ async def test_modules_coordinator_first_run(
|
||||
hass,
|
||||
dt_util.as_utc(datetime(1970, 1, 1, 0, 0)),
|
||||
None,
|
||||
{f"{DOMAIN}:{SITE_ID}_1001", f"{DOMAIN}:{SITE_ID}_1002"},
|
||||
{f"{DOMAIN}:{SITE_ID}_7a012345_ca", f"{DOMAIN}:{SITE_ID}_7a012346_ca"},
|
||||
"hour",
|
||||
None,
|
||||
{"state", "sum"},
|
||||
)
|
||||
assert stats == {
|
||||
f"{DOMAIN}:{SITE_ID}_1001": [
|
||||
{"start": 1735783200.0, "end": 1735786800.0, "state": 11.5, "sum": 11.5},
|
||||
{"start": 1735786800.0, "end": 1735790400.0, "state": 15.5, "sum": 27.0},
|
||||
f"{DOMAIN}:{SITE_ID}_7a012345_ca": [
|
||||
{"start": 1735783200.0, "end": 1735786800.0, "state": 10.0, "sum": 10.0},
|
||||
{"start": 1735786800.0, "end": 1735790400.0, "state": 14.0, "sum": 24.0},
|
||||
],
|
||||
f"{DOMAIN}:{SITE_ID}_1002": [
|
||||
{"start": 1735783200.0, "end": 1735786800.0, "state": 21.5, "sum": 21.5},
|
||||
{"start": 1735786800.0, "end": 1735790400.0, "state": 25.5, "sum": 47.0},
|
||||
f"{DOMAIN}:{SITE_ID}_7a012346_ca": [
|
||||
{"start": 1735783200.0, "end": 1735786800.0, "state": 20.0, "sum": 20.0},
|
||||
{"start": 1735786800.0, "end": 1735790400.0, "state": 24.0, "sum": 44.0},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -254,7 +239,7 @@ async def test_modules_coordinator_subsequent_run(
|
||||
) -> None:
|
||||
"""Test the coordinator correctly updates statistics on subsequent runs."""
|
||||
mock_solar_edge_web.async_get_equipment.return_value = {
|
||||
1001: {"displayName": "1.1"},
|
||||
"7A012345-CA": {"name": "Optimizer 1.1"},
|
||||
}
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
@@ -269,36 +254,12 @@ async def test_modules_coordinator_subsequent_run(
|
||||
# Updated values, different from the first run
|
||||
EnergyData(
|
||||
start_time=dt_util.as_utc(datetime(2025, 1, 1, 11, 0)),
|
||||
values={1001: 24.0},
|
||||
),
|
||||
EnergyData(
|
||||
start_time=dt_util.as_utc(datetime(2025, 1, 1, 11, 15)),
|
||||
values={1001: 25.0},
|
||||
),
|
||||
EnergyData(
|
||||
start_time=dt_util.as_utc(datetime(2025, 1, 1, 11, 30)),
|
||||
values={1001: 26.0},
|
||||
),
|
||||
EnergyData(
|
||||
start_time=dt_util.as_utc(datetime(2025, 1, 1, 11, 45)),
|
||||
values={1001: 27.0},
|
||||
values={"7A012345-CA": 24.0},
|
||||
),
|
||||
# New values for the next hour
|
||||
EnergyData(
|
||||
start_time=dt_util.as_utc(datetime(2025, 1, 1, 12, 0)),
|
||||
values={1001: 28.0},
|
||||
),
|
||||
EnergyData(
|
||||
start_time=dt_util.as_utc(datetime(2025, 1, 1, 12, 15)),
|
||||
values={1001: 29.0},
|
||||
),
|
||||
EnergyData(
|
||||
start_time=dt_util.as_utc(datetime(2025, 1, 1, 12, 30)),
|
||||
values={1001: 30.0},
|
||||
),
|
||||
EnergyData(
|
||||
start_time=dt_util.as_utc(datetime(2025, 1, 1, 12, 45)),
|
||||
values={1001: 31.0},
|
||||
values={"7A012345-CA": 28.0},
|
||||
),
|
||||
]
|
||||
|
||||
@@ -313,16 +274,16 @@ async def test_modules_coordinator_subsequent_run(
|
||||
hass,
|
||||
dt_util.as_utc(datetime(1970, 1, 1, 0, 0)),
|
||||
None,
|
||||
{f"{DOMAIN}:{SITE_ID}_1001"},
|
||||
{f"{DOMAIN}:{SITE_ID}_7a012345_ca"},
|
||||
"hour",
|
||||
None,
|
||||
{"state", "sum"},
|
||||
)
|
||||
assert stats == {
|
||||
f"{DOMAIN}:{SITE_ID}_1001": [
|
||||
{"start": 1735783200.0, "end": 1735786800.0, "state": 11.5, "sum": 11.5},
|
||||
{"start": 1735786800.0, "end": 1735790400.0, "state": 25.5, "sum": 37.0},
|
||||
{"start": 1735790400.0, "end": 1735794000.0, "state": 29.5, "sum": 66.5},
|
||||
f"{DOMAIN}:{SITE_ID}_7a012345_ca": [
|
||||
{"start": 1735783200.0, "end": 1735786800.0, "state": 10.0, "sum": 10.0},
|
||||
{"start": 1735786800.0, "end": 1735790400.0, "state": 24.0, "sum": 34.0},
|
||||
{"start": 1735790400.0, "end": 1735794000.0, "state": 28.0, "sum": 62.0},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -335,7 +296,7 @@ async def test_modules_coordinator_subsequent_run_with_gap(
|
||||
) -> None:
|
||||
"""Test the coordinator updates statistics with a gap in data."""
|
||||
mock_solar_edge_web.async_get_equipment.return_value = {
|
||||
1001: {"displayName": "1.1"},
|
||||
"7A012345-CA": {"name": "Optimizer 1.1"},
|
||||
}
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
@@ -350,19 +311,7 @@ async def test_modules_coordinator_subsequent_run_with_gap(
|
||||
# New values a month later, simulating a gap in data
|
||||
EnergyData(
|
||||
start_time=dt_util.as_utc(datetime(2025, 2, 1, 11, 0)),
|
||||
values={1001: 24.0},
|
||||
),
|
||||
EnergyData(
|
||||
start_time=dt_util.as_utc(datetime(2025, 2, 1, 11, 15)),
|
||||
values={1001: 25.0},
|
||||
),
|
||||
EnergyData(
|
||||
start_time=dt_util.as_utc(datetime(2025, 2, 1, 11, 30)),
|
||||
values={1001: 26.0},
|
||||
),
|
||||
EnergyData(
|
||||
start_time=dt_util.as_utc(datetime(2025, 2, 1, 11, 45)),
|
||||
values={1001: 27.0},
|
||||
values={"7A012345-CA": 24.0},
|
||||
),
|
||||
]
|
||||
|
||||
@@ -376,16 +325,16 @@ async def test_modules_coordinator_subsequent_run_with_gap(
|
||||
hass,
|
||||
dt_util.as_utc(datetime(1970, 1, 1, 0, 0)),
|
||||
None,
|
||||
{f"{DOMAIN}:{SITE_ID}_1001"},
|
||||
{f"{DOMAIN}:{SITE_ID}_7a012345_ca"},
|
||||
"hour",
|
||||
None,
|
||||
{"state", "sum"},
|
||||
)
|
||||
assert stats == {
|
||||
f"{DOMAIN}:{SITE_ID}_1001": [
|
||||
{"start": 1735783200.0, "end": 1735786800.0, "state": 11.5, "sum": 11.5},
|
||||
{"start": 1735786800.0, "end": 1735790400.0, "state": 15.5, "sum": 27.0},
|
||||
{"start": 1738465200.0, "end": 1738468800.0, "state": 25.5, "sum": 52.5},
|
||||
f"{DOMAIN}:{SITE_ID}_7a012345_ca": [
|
||||
{"start": 1735783200.0, "end": 1735786800.0, "state": 10.0, "sum": 10.0},
|
||||
{"start": 1735786800.0, "end": 1735790400.0, "state": 14.0, "sum": 24.0},
|
||||
{"start": 1738465200.0, "end": 1738468800.0, "state": 24.0, "sum": 48.0},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -415,7 +364,7 @@ async def test_modules_coordinator_no_energy_data(
|
||||
hass,
|
||||
dt_util.as_utc(datetime(1970, 1, 1, 0, 0)),
|
||||
None,
|
||||
{f"{DOMAIN}:{SITE_ID}_1001", f"{DOMAIN}:{SITE_ID}_1002"},
|
||||
{f"{DOMAIN}:{SITE_ID}_7a012345_ca", f"{DOMAIN}:{SITE_ID}_7a012346_ca"},
|
||||
"hour",
|
||||
None,
|
||||
{"state", "sum"},
|
||||
@@ -435,3 +384,117 @@ async def test_modules_coordinator_api_failure(
|
||||
await setup_integration(hass, mock_config_entry_web_login)
|
||||
|
||||
assert mock_config_entry_web_login.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("recorder_mock")
|
||||
async def test_legacy_statistic_id_reused(
|
||||
hass: HomeAssistant,
|
||||
mock_solar_edge_web: AsyncMock,
|
||||
) -> None:
|
||||
"""Test that legacy numeric statistic IDs are reused via name matching."""
|
||||
# Add a legacy numeric statistic with name "1.1" (matching equipment name).
|
||||
legacy_statistic_id = f"{DOMAIN}:{SITE_ID}_231397259"
|
||||
legacy_metadata = StatisticMetaData(
|
||||
mean_type=StatisticMeanType.ARITHMETIC,
|
||||
has_sum=True,
|
||||
name="SolarEdge 1.1",
|
||||
source=DOMAIN,
|
||||
statistic_id=legacy_statistic_id,
|
||||
unit_class=EnergyConverter.UNIT_CLASS,
|
||||
unit_of_measurement="Wh",
|
||||
)
|
||||
async_add_external_statistics(hass, legacy_metadata, [])
|
||||
await async_wait_recording_done(hass)
|
||||
|
||||
# Equipment with matching name "1.1".
|
||||
mock_solar_edge_web.async_get_equipment.return_value = {
|
||||
"7A012345-CA": {"name": "Optimizer 1.1"},
|
||||
}
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="SolarEdge",
|
||||
data={CONF_SITE_ID: SITE_ID, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD},
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
await async_wait_recording_done(hass)
|
||||
|
||||
# The legacy statistic ID should be reused (not a new serial-based one).
|
||||
coordinator: SolarEdgeModulesCoordinator = entry.runtime_data[
|
||||
DATA_MODULES_COORDINATOR
|
||||
]
|
||||
assert coordinator.get_statistic_id("7A012345-CA") == legacy_statistic_id
|
||||
|
||||
# Data should be inserted under the legacy ID.
|
||||
stats = await hass.async_add_executor_job(
|
||||
statistics_during_period,
|
||||
hass,
|
||||
dt_util.as_utc(datetime(1970, 1, 1, 0, 0)),
|
||||
None,
|
||||
{legacy_statistic_id},
|
||||
"hour",
|
||||
None,
|
||||
{"state", "sum"},
|
||||
)
|
||||
assert stats == {
|
||||
legacy_statistic_id: [
|
||||
{"start": 1735783200.0, "end": 1735786800.0, "state": 10.0, "sum": 10.0},
|
||||
{"start": 1735786800.0, "end": 1735790400.0, "state": 14.0, "sum": 24.0},
|
||||
]
|
||||
}
|
||||
# No new serial-based ID should have been created.
|
||||
all_stats = await async_list_statistic_ids(hass)
|
||||
stat_ids = {s["statistic_id"] for s in all_stats}
|
||||
assert f"{DOMAIN}:{SITE_ID}_7a012345_ca" not in stat_ids
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("recorder_mock")
|
||||
async def test_legacy_statistic_id_ambiguous(
|
||||
hass: HomeAssistant,
|
||||
mock_solar_edge_web: AsyncMock,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test that ambiguous legacy statistic names are not reused."""
|
||||
# Two legacy numeric statistics with the same name, e.g. after a module
|
||||
# replacement created a new series under a new numeric ID.
|
||||
for legacy_id in (
|
||||
f"{DOMAIN}:{SITE_ID}_231397259",
|
||||
f"{DOMAIN}:{SITE_ID}_231397260",
|
||||
):
|
||||
legacy_metadata = StatisticMetaData(
|
||||
mean_type=StatisticMeanType.ARITHMETIC,
|
||||
has_sum=True,
|
||||
name="SolarEdge 1.1",
|
||||
source=DOMAIN,
|
||||
statistic_id=legacy_id,
|
||||
unit_class=EnergyConverter.UNIT_CLASS,
|
||||
unit_of_measurement="Wh",
|
||||
)
|
||||
async_add_external_statistics(hass, legacy_metadata, [])
|
||||
await async_wait_recording_done(hass)
|
||||
|
||||
# Equipment with matching name "1.1".
|
||||
mock_solar_edge_web.async_get_equipment.return_value = {
|
||||
"7A012345-CA": {"name": "Optimizer 1.1"},
|
||||
}
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="SolarEdge",
|
||||
data={CONF_SITE_ID: SITE_ID, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD},
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
await async_wait_recording_done(hass)
|
||||
|
||||
# The match is ambiguous, so a new serial-based ID is used.
|
||||
coordinator: SolarEdgeModulesCoordinator = entry.runtime_data[
|
||||
DATA_MODULES_COORDINATOR
|
||||
]
|
||||
assert (
|
||||
coordinator.get_statistic_id("7A012345-CA") == f"{DOMAIN}:{SITE_ID}_7a012345_ca"
|
||||
)
|
||||
assert "ambiguous" in caplog.text
|
||||
|
||||
Reference in New Issue
Block a user