mirror of
https://github.com/home-assistant/core.git
synced 2026-05-18 06:20:17 +01:00
5b2816e56c
Co-authored-by: Simone Chemelli <simone.chemelli@gmail.com>
702 lines
24 KiB
Python
702 lines
24 KiB
Python
"""Tests for the SolarEdge sensor platform."""
|
|
|
|
from unittest.mock import AsyncMock, Mock, patch
|
|
|
|
from aiohttp import ClientError
|
|
from freezegun.api import FrozenDateTimeFactory
|
|
import pytest
|
|
from syrupy.assertion import SnapshotAssertion
|
|
|
|
from homeassistant.components.recorder import Recorder
|
|
from homeassistant.components.solaredge.const import DOMAIN, INVENTORY_UPDATE_DELAY
|
|
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.helpers import entity_registry as er
|
|
|
|
from . import setup_integration
|
|
from .conftest import SITE_ID
|
|
|
|
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
|
|
|
STORAGE_DATA_MULTI_BATTERY = {
|
|
"storageData": {
|
|
"batteries": [
|
|
{
|
|
"serialNumber": "BAT001",
|
|
"telemetries": [
|
|
{
|
|
"timeStamp": "2025-01-01 00:00:00",
|
|
"lifeTimeEnergyCharged": 1000.0,
|
|
"lifeTimeEnergyDischarged": 500.0,
|
|
"batteryPercentageState": 50.0,
|
|
"power": 100.0,
|
|
},
|
|
{
|
|
"timeStamp": "2025-01-01 12:00:00",
|
|
"lifeTimeEnergyCharged": 1500.0,
|
|
"lifeTimeEnergyDischarged": 800.0,
|
|
"batteryPercentageState": 75.0,
|
|
"power": 200.0,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
"serialNumber": "BAT002",
|
|
"telemetries": [
|
|
{
|
|
"timeStamp": "2025-01-01 00:00:00",
|
|
"lifeTimeEnergyCharged": 2000.0,
|
|
"lifeTimeEnergyDischarged": 1000.0,
|
|
"batteryPercentageState": 40.0,
|
|
"power": 150.0,
|
|
},
|
|
{
|
|
"timeStamp": "2025-01-01 12:00:00",
|
|
"lifeTimeEnergyCharged": 2700.0,
|
|
"lifeTimeEnergyDischarged": 1400.0,
|
|
"batteryPercentageState": 80.0,
|
|
"power": 250.0,
|
|
},
|
|
],
|
|
},
|
|
]
|
|
}
|
|
}
|
|
|
|
|
|
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
|
async def test_all_entities(
|
|
recorder_mock: Recorder,
|
|
hass: HomeAssistant,
|
|
snapshot: SnapshotAssertion,
|
|
entity_registry: er.EntityRegistry,
|
|
mock_config_entry: MockConfigEntry,
|
|
solaredge_api: Mock,
|
|
) -> None:
|
|
"""Test all sensors are created with correct state and registry."""
|
|
with patch("homeassistant.components.solaredge.PLATFORMS", [Platform.SENSOR]):
|
|
await setup_integration(hass, mock_config_entry)
|
|
|
|
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
|
|
|
|
|
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
|
async def test_overview_sensors_unavailable_on_api_error(
|
|
recorder_mock: Recorder,
|
|
hass: HomeAssistant,
|
|
mock_config_entry: MockConfigEntry,
|
|
solaredge_api: Mock,
|
|
) -> None:
|
|
"""Test overview-based sensors are unavailable when overview API fails."""
|
|
solaredge_api.get_overview.side_effect = ClientError()
|
|
|
|
await setup_integration(hass, mock_config_entry)
|
|
|
|
for sensor_id in (
|
|
"sensor.solaredge_lifetime_energy",
|
|
"sensor.solaredge_energy_this_year",
|
|
"sensor.solaredge_energy_this_month",
|
|
"sensor.solaredge_energy_today",
|
|
"sensor.solaredge_current_power",
|
|
):
|
|
state = hass.states.get(sensor_id)
|
|
assert state is not None, sensor_id
|
|
assert state.state == STATE_UNAVAILABLE, sensor_id
|
|
|
|
|
|
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
|
async def test_storage_level_unknown_when_storage_missing(
|
|
recorder_mock: Recorder,
|
|
hass: HomeAssistant,
|
|
mock_config_entry: MockConfigEntry,
|
|
solaredge_api: Mock,
|
|
) -> None:
|
|
"""Test storage_level returns None when site has no storage."""
|
|
power_flow = solaredge_api.get_current_power_flow.return_value
|
|
power_flow["siteCurrentPowerFlow"].pop("STORAGE")
|
|
# Drop STORAGE from connections too so the data service does not reference it.
|
|
power_flow["siteCurrentPowerFlow"]["connections"] = [
|
|
{"from": "GRID", "to": "Load"},
|
|
{"from": "PV", "to": "Load"},
|
|
]
|
|
|
|
await setup_integration(hass, mock_config_entry)
|
|
|
|
state = hass.states.get("sensor.solaredge_storage_level")
|
|
assert state is not None
|
|
assert state.state == STATE_UNKNOWN
|
|
|
|
|
|
async def test_no_sensors_without_api_key(
|
|
recorder_mock: Recorder,
|
|
hass: HomeAssistant,
|
|
mock_config_entry_web_login: MockConfigEntry,
|
|
solaredge_web_api: AsyncMock,
|
|
entity_registry: er.EntityRegistry,
|
|
) -> None:
|
|
"""Test no sensors are created when only web login auth is configured."""
|
|
await setup_integration(hass, mock_config_entry_web_login)
|
|
|
|
entries = er.async_entries_for_config_entry(
|
|
entity_registry, mock_config_entry_web_login.entry_id
|
|
)
|
|
assert entries == []
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("api_method", "sensor_id"),
|
|
[
|
|
("get_overview", "sensor.solaredge_lifetime_energy"),
|
|
("get_inventory", "sensor.solaredge_inverters"),
|
|
("get_current_power_flow", "sensor.solaredge_grid_power"),
|
|
("get_energy_details", "sensor.solaredge_produced_energy"),
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
|
async def test_sensor_unavailable_on_data_service_keyerror(
|
|
recorder_mock: Recorder,
|
|
hass: HomeAssistant,
|
|
mock_config_entry: MockConfigEntry,
|
|
solaredge_api: Mock,
|
|
api_method: str,
|
|
sensor_id: str,
|
|
) -> None:
|
|
"""Test sensors become unavailable on UpdateFailed."""
|
|
getattr(solaredge_api, api_method).return_value = {}
|
|
|
|
await setup_integration(hass, mock_config_entry)
|
|
|
|
state = hass.states.get(sensor_id)
|
|
assert state is not None
|
|
assert state.state == STATE_UNAVAILABLE
|
|
|
|
|
|
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
|
async def test_details_sensor_unavailable_on_data_service_keyerror(
|
|
recorder_mock: Recorder,
|
|
hass: HomeAssistant,
|
|
mock_config_entry: MockConfigEntry,
|
|
solaredge_api: Mock,
|
|
) -> None:
|
|
"""Test the details sensor becomes unavailable when its refresh fails.
|
|
|
|
`get_details` is also called during setup validation, so the first call
|
|
must succeed; subsequent calls (the data service refresh) return data
|
|
without the 'details' key to trigger UpdateFailed.
|
|
"""
|
|
solaredge_api.get_details.side_effect = [
|
|
{"details": {"status": "Active"}},
|
|
{},
|
|
]
|
|
|
|
await setup_integration(hass, mock_config_entry)
|
|
|
|
state = hass.states.get("sensor.solaredge_site_details")
|
|
assert state is not None
|
|
assert state.state == STATE_UNAVAILABLE
|
|
|
|
|
|
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
|
async def test_energy_details_sensor_unknown_when_no_meters(
|
|
recorder_mock: Recorder,
|
|
hass: HomeAssistant,
|
|
mock_config_entry: MockConfigEntry,
|
|
solaredge_api: Mock,
|
|
) -> None:
|
|
"""Test energy detail sensors stay unknown when the API reports no meters."""
|
|
solaredge_api.get_energy_details.return_value = {"energyDetails": {"unit": "Wh"}}
|
|
|
|
await setup_integration(hass, mock_config_entry)
|
|
|
|
state = hass.states.get("sensor.solaredge_produced_energy")
|
|
assert state is not None
|
|
assert state.state == STATE_UNKNOWN
|
|
|
|
|
|
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
|
async def test_energy_details_filters_meters(
|
|
recorder_mock: Recorder,
|
|
hass: HomeAssistant,
|
|
mock_config_entry: MockConfigEntry,
|
|
solaredge_api: Mock,
|
|
) -> None:
|
|
"""Test energy details skips meters without type/values."""
|
|
solaredge_api.get_energy_details.return_value = {
|
|
"energyDetails": {
|
|
"unit": "Wh",
|
|
"meters": [
|
|
{"type": "Production"}, # missing values, skipped
|
|
{"values": [{"date": "2025-01-01", "value": 1.0}]}, # missing type
|
|
{
|
|
"type": "SomethingElse", # unsupported type, skipped
|
|
"values": [{"date": "2025-01-01", "value": 2.0}],
|
|
},
|
|
{
|
|
"type": "Production",
|
|
"values": [{"date": "2025-01-01", "value": 100.0}],
|
|
},
|
|
],
|
|
}
|
|
}
|
|
|
|
await setup_integration(hass, mock_config_entry)
|
|
|
|
produced = hass.states.get("sensor.solaredge_produced_energy")
|
|
assert produced is not None
|
|
assert produced.state == "100.0"
|
|
assert produced.attributes["date"] == "2025-01-01"
|
|
|
|
consumed = hass.states.get("sensor.solaredge_consumed_energy")
|
|
assert consumed is not None
|
|
assert consumed.state == STATE_UNKNOWN
|
|
|
|
|
|
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
|
async def test_power_flow_sensor_unknown_when_no_connections(
|
|
recorder_mock: Recorder,
|
|
hass: HomeAssistant,
|
|
mock_config_entry: MockConfigEntry,
|
|
solaredge_api: Mock,
|
|
) -> None:
|
|
"""Test power flow sensors stay unknown when the API reports no connections."""
|
|
solaredge_api.get_current_power_flow.return_value = {
|
|
"siteCurrentPowerFlow": {"unit": "W"}
|
|
}
|
|
|
|
await setup_integration(hass, mock_config_entry)
|
|
|
|
state = hass.states.get("sensor.solaredge_grid_power")
|
|
assert state is not None
|
|
assert state.state == STATE_UNKNOWN
|
|
|
|
|
|
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
|
async def test_power_flow_grid_export_storage_discharge(
|
|
recorder_mock: Recorder,
|
|
hass: HomeAssistant,
|
|
mock_config_entry: MockConfigEntry,
|
|
solaredge_api: Mock,
|
|
) -> None:
|
|
"""Test power flow sign flipping for grid export and storage."""
|
|
solaredge_api.get_current_power_flow.return_value = {
|
|
"siteCurrentPowerFlow": {
|
|
"unit": "W",
|
|
"connections": [
|
|
{"from": "PV", "to": "GRID"},
|
|
{"from": "STORAGE", "to": "Load"},
|
|
],
|
|
"GRID": {"status": "Active", "currentPower": 100.0},
|
|
"LOAD": {"status": "Active", "currentPower": 500.0},
|
|
"PV": {"status": "Active", "currentPower": 600.0},
|
|
"STORAGE": {
|
|
"status": "Discharging",
|
|
"currentPower": 400.0,
|
|
"chargeLevel": 60,
|
|
},
|
|
}
|
|
}
|
|
|
|
await setup_integration(hass, mock_config_entry)
|
|
|
|
grid = hass.states.get("sensor.solaredge_grid_power")
|
|
assert grid is not None
|
|
assert grid.state == "-100.0"
|
|
assert grid.attributes["flow"] == "export"
|
|
|
|
storage = hass.states.get("sensor.solaredge_storage_power")
|
|
assert storage is not None
|
|
assert storage.state == "400.0"
|
|
assert storage.attributes["flow"] == "discharge"
|
|
assert storage.attributes["soc"] == 60
|
|
|
|
|
|
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
|
async def test_power_flow_zero_current_power_keeps_zero(
|
|
recorder_mock: Recorder,
|
|
hass: HomeAssistant,
|
|
mock_config_entry: MockConfigEntry,
|
|
solaredge_api: Mock,
|
|
) -> None:
|
|
"""Test power flow leaves zero values untouched (no -0 in the state)."""
|
|
solaredge_api.get_current_power_flow.return_value = {
|
|
"siteCurrentPowerFlow": {
|
|
"unit": "W",
|
|
"connections": [{"from": "PV", "to": "GRID"}],
|
|
"GRID": {"status": "Idle", "currentPower": 0},
|
|
"STORAGE": {"status": "Idle", "currentPower": 0, "chargeLevel": 50},
|
|
}
|
|
}
|
|
|
|
await setup_integration(hass, mock_config_entry)
|
|
|
|
assert hass.states.get("sensor.solaredge_grid_power").state == "0"
|
|
assert hass.states.get("sensor.solaredge_storage_power").state == "0"
|
|
|
|
|
|
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
|
async def test_storage_data_service(
|
|
recorder_mock: Recorder,
|
|
hass: HomeAssistant,
|
|
mock_config_entry: MockConfigEntry,
|
|
solaredge_api: Mock,
|
|
entity_registry: er.EntityRegistry,
|
|
) -> None:
|
|
"""Test storage data service fetches battery charge/discharge energy."""
|
|
await setup_integration(hass, mock_config_entry)
|
|
|
|
# Aggregate sensors
|
|
charge_entry = entity_registry.async_get_entity_id(
|
|
"sensor", DOMAIN, f"{SITE_ID}_storage_charge_energy"
|
|
)
|
|
discharge_entry = entity_registry.async_get_entity_id(
|
|
"sensor", DOMAIN, f"{SITE_ID}_storage_discharge_energy"
|
|
)
|
|
assert charge_entry is not None
|
|
assert discharge_entry is not None
|
|
|
|
state = hass.states.get(charge_entry)
|
|
assert state is not None
|
|
assert float(state.state) == 500.0 # 1500 - 1000
|
|
|
|
state = hass.states.get(discharge_entry)
|
|
assert state is not None
|
|
assert float(state.state) == 300.0 # 800 - 500
|
|
|
|
# Per-battery entities for BAT001
|
|
bat_charge = entity_registry.async_get_entity_id(
|
|
"sensor", DOMAIN, f"{SITE_ID}_BAT001_battery_charge_energy"
|
|
)
|
|
bat_discharge = entity_registry.async_get_entity_id(
|
|
"sensor", DOMAIN, f"{SITE_ID}_BAT001_battery_discharge_energy"
|
|
)
|
|
bat_soc = entity_registry.async_get_entity_id(
|
|
"sensor", DOMAIN, f"{SITE_ID}_BAT001_battery_state_of_charge"
|
|
)
|
|
bat_power = entity_registry.async_get_entity_id(
|
|
"sensor", DOMAIN, f"{SITE_ID}_BAT001_battery_power"
|
|
)
|
|
assert bat_charge is not None
|
|
assert bat_discharge is not None
|
|
assert bat_soc is not None
|
|
assert bat_power is not None
|
|
|
|
state = hass.states.get(bat_charge)
|
|
assert state is not None
|
|
assert float(state.state) == 500.0
|
|
|
|
state = hass.states.get(bat_discharge)
|
|
assert state is not None
|
|
assert float(state.state) == 300.0
|
|
|
|
state = hass.states.get(bat_soc)
|
|
assert state is not None
|
|
assert float(state.state) == 75.0
|
|
|
|
state = hass.states.get(bat_power)
|
|
assert state is not None
|
|
assert float(state.state) == 200.0
|
|
|
|
|
|
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
|
async def test_storage_data_service_multi_battery(
|
|
recorder_mock: Recorder,
|
|
hass: HomeAssistant,
|
|
mock_config_entry: MockConfigEntry,
|
|
solaredge_api: Mock,
|
|
entity_registry: er.EntityRegistry,
|
|
) -> None:
|
|
"""Test storage data service aggregates data across multiple batteries."""
|
|
inventory = solaredge_api.get_inventory.return_value
|
|
inventory["Inventory"]["batteries"] = [{"SN": "BAT001"}, {"SN": "BAT002"}]
|
|
solaredge_api.get_storage_data.return_value = STORAGE_DATA_MULTI_BATTERY
|
|
|
|
await setup_integration(hass, mock_config_entry)
|
|
|
|
charge_entry = entity_registry.async_get_entity_id(
|
|
"sensor", DOMAIN, f"{SITE_ID}_storage_charge_energy"
|
|
)
|
|
discharge_entry = entity_registry.async_get_entity_id(
|
|
"sensor", DOMAIN, f"{SITE_ID}_storage_discharge_energy"
|
|
)
|
|
assert charge_entry is not None
|
|
assert discharge_entry is not None
|
|
|
|
# BAT001: charge=500 (1500-1000), discharge=300 (800-500)
|
|
# BAT002: charge=700 (2700-2000), discharge=400 (1400-1000)
|
|
assert float(hass.states.get(charge_entry).state) == 1200.0
|
|
assert float(hass.states.get(discharge_entry).state) == 700.0
|
|
|
|
bat1_soc = entity_registry.async_get_entity_id(
|
|
"sensor", DOMAIN, f"{SITE_ID}_BAT001_battery_state_of_charge"
|
|
)
|
|
assert bat1_soc is not None
|
|
assert float(hass.states.get(bat1_soc).state) == 75.0
|
|
|
|
bat2_charge = entity_registry.async_get_entity_id(
|
|
"sensor", DOMAIN, f"{SITE_ID}_BAT002_battery_charge_energy"
|
|
)
|
|
bat2_soc = entity_registry.async_get_entity_id(
|
|
"sensor", DOMAIN, f"{SITE_ID}_BAT002_battery_state_of_charge"
|
|
)
|
|
assert bat2_charge is not None
|
|
assert bat2_soc is not None
|
|
assert float(hass.states.get(bat2_charge).state) == 700.0
|
|
assert float(hass.states.get(bat2_soc).state) == 80.0
|
|
|
|
|
|
async def test_storage_service_not_created_when_inventory_has_no_batteries(
|
|
recorder_mock: Recorder,
|
|
hass: HomeAssistant,
|
|
mock_config_entry: MockConfigEntry,
|
|
solaredge_api: Mock,
|
|
entity_registry: er.EntityRegistry,
|
|
) -> None:
|
|
"""Test storage service is not created when no batteries in inventory."""
|
|
inventory = solaredge_api.get_inventory.return_value
|
|
inventory["Inventory"]["batteries"] = []
|
|
|
|
await setup_integration(hass, mock_config_entry)
|
|
|
|
charge_entry = entity_registry.async_get_entity_id(
|
|
"sensor", DOMAIN, f"{SITE_ID}_storage_charge_energy"
|
|
)
|
|
discharge_entry = entity_registry.async_get_entity_id(
|
|
"sensor", DOMAIN, f"{SITE_ID}_storage_discharge_energy"
|
|
)
|
|
assert charge_entry is None
|
|
assert discharge_entry is None
|
|
|
|
|
|
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
|
async def test_storage_data_service_api_error(
|
|
recorder_mock: Recorder,
|
|
hass: HomeAssistant,
|
|
mock_config_entry: MockConfigEntry,
|
|
solaredge_api: Mock,
|
|
entity_registry: er.EntityRegistry,
|
|
) -> None:
|
|
"""Test storage sensors are unavailable when the storage API errors out."""
|
|
solaredge_api.get_storage_data.side_effect = Exception("API error")
|
|
|
|
await setup_integration(hass, mock_config_entry)
|
|
|
|
charge_entry = entity_registry.async_get_entity_id(
|
|
"sensor", DOMAIN, f"{SITE_ID}_storage_charge_energy"
|
|
)
|
|
discharge_entry = entity_registry.async_get_entity_id(
|
|
"sensor", DOMAIN, f"{SITE_ID}_storage_discharge_energy"
|
|
)
|
|
assert charge_entry is not None
|
|
assert discharge_entry is not None
|
|
|
|
assert hass.states.get(charge_entry).state == STATE_UNAVAILABLE
|
|
assert hass.states.get(discharge_entry).state == STATE_UNAVAILABLE
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"bad_response",
|
|
[{"unexpected": {}}, {"storageData": {"otherField": "value"}}],
|
|
ids=["missing_storageData", "missing_batteries"],
|
|
)
|
|
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
|
async def test_storage_data_missing_keys_in_response(
|
|
recorder_mock: Recorder,
|
|
hass: HomeAssistant,
|
|
mock_config_entry: MockConfigEntry,
|
|
solaredge_api: Mock,
|
|
entity_registry: er.EntityRegistry,
|
|
bad_response: dict,
|
|
) -> None:
|
|
"""Test storage sensors unavailable with missing required keys."""
|
|
solaredge_api.get_storage_data.return_value = bad_response
|
|
|
|
await setup_integration(hass, mock_config_entry)
|
|
|
|
charge_entry = entity_registry.async_get_entity_id(
|
|
"sensor", DOMAIN, f"{SITE_ID}_storage_charge_energy"
|
|
)
|
|
discharge_entry = entity_registry.async_get_entity_id(
|
|
"sensor", DOMAIN, f"{SITE_ID}_storage_discharge_energy"
|
|
)
|
|
assert charge_entry is not None
|
|
assert discharge_entry is not None
|
|
assert hass.states.get(charge_entry).state == STATE_UNAVAILABLE
|
|
assert hass.states.get(discharge_entry).state == STATE_UNAVAILABLE
|
|
|
|
|
|
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
|
async def test_storage_service_deferred_after_inventory_failure(
|
|
recorder_mock: Recorder,
|
|
hass: HomeAssistant,
|
|
freezer: FrozenDateTimeFactory,
|
|
mock_config_entry: MockConfigEntry,
|
|
solaredge_api: Mock,
|
|
entity_registry: er.EntityRegistry,
|
|
) -> None:
|
|
"""Test storage service is created after inventory recovers from a failure."""
|
|
valid_inventory = solaredge_api.get_inventory.return_value
|
|
solaredge_api.get_inventory.side_effect = KeyError("Inventory")
|
|
|
|
await setup_integration(hass, mock_config_entry)
|
|
|
|
# Storage sensors are not created yet — the inventory fetch failed.
|
|
assert (
|
|
entity_registry.async_get_entity_id(
|
|
"sensor", DOMAIN, f"{SITE_ID}_storage_charge_energy"
|
|
)
|
|
is None
|
|
)
|
|
|
|
# Inventory recovers and reports a battery → storage sensors get created.
|
|
solaredge_api.get_inventory.side_effect = None
|
|
solaredge_api.get_inventory.return_value = valid_inventory
|
|
|
|
freezer.tick(INVENTORY_UPDATE_DELAY)
|
|
async_fire_time_changed(hass)
|
|
await hass.async_block_till_done(wait_background_tasks=True)
|
|
|
|
assert (
|
|
entity_registry.async_get_entity_id(
|
|
"sensor", DOMAIN, f"{SITE_ID}_storage_charge_energy"
|
|
)
|
|
is not None
|
|
)
|
|
assert (
|
|
entity_registry.async_get_entity_id(
|
|
"sensor", DOMAIN, f"{SITE_ID}_storage_discharge_energy"
|
|
)
|
|
is not None
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("storage_response", "expected_charge_state"),
|
|
[
|
|
# Empty batteries list → data service returns early, aggregate stays unset.
|
|
({"storageData": {"batteries": []}}, STATE_UNKNOWN),
|
|
# Battery missing the serialNumber key → skipped in the loop, aggregate
|
|
# falls through with the initial 0.0 totals.
|
|
({"storageData": {"batteries": [{"telemetries": []}]}}, "0.0"),
|
|
# Battery with no telemetries → skipped after the serial check.
|
|
(
|
|
{
|
|
"storageData": {
|
|
"batteries": [{"serialNumber": "BAT001", "telemetries": []}]
|
|
}
|
|
},
|
|
"0.0",
|
|
),
|
|
# Battery with a single telemetry → can't compute a delta, contributes
|
|
# 0.0 to the aggregate via the len < 2 branch.
|
|
(
|
|
{
|
|
"storageData": {
|
|
"batteries": [
|
|
{
|
|
"serialNumber": "BAT001",
|
|
"telemetries": [
|
|
{
|
|
"timeStamp": "2025-01-01 00:00:00",
|
|
"lifeTimeEnergyCharged": 1000.0,
|
|
"lifeTimeEnergyDischarged": 500.0,
|
|
"batteryPercentageState": 50.0,
|
|
"power": 100.0,
|
|
}
|
|
],
|
|
}
|
|
]
|
|
}
|
|
},
|
|
"0.0",
|
|
),
|
|
],
|
|
ids=[
|
|
"empty_batteries",
|
|
"battery_without_serial",
|
|
"battery_without_telemetries",
|
|
"battery_with_single_telemetry",
|
|
],
|
|
)
|
|
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
|
async def test_storage_data_service_handles_malformed_responses(
|
|
recorder_mock: Recorder,
|
|
hass: HomeAssistant,
|
|
mock_config_entry: MockConfigEntry,
|
|
solaredge_api: Mock,
|
|
entity_registry: er.EntityRegistry,
|
|
storage_response: dict,
|
|
expected_charge_state: str,
|
|
) -> None:
|
|
"""Test storage tolerates batteries without serial/telemetries."""
|
|
solaredge_api.get_storage_data.return_value = storage_response
|
|
|
|
await setup_integration(hass, mock_config_entry)
|
|
|
|
charge_entry = entity_registry.async_get_entity_id(
|
|
"sensor", DOMAIN, f"{SITE_ID}_storage_charge_energy"
|
|
)
|
|
assert charge_entry is not None
|
|
state = hass.states.get(charge_entry)
|
|
assert state is not None
|
|
assert state.state == expected_charge_state
|
|
|
|
|
|
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
|
async def test_inventory_battery_without_serial_skipped(
|
|
recorder_mock: Recorder,
|
|
hass: HomeAssistant,
|
|
mock_config_entry: MockConfigEntry,
|
|
solaredge_api: Mock,
|
|
entity_registry: er.EntityRegistry,
|
|
) -> None:
|
|
"""Test batteries without serial are skipped for per-battery sensors."""
|
|
inventory = solaredge_api.get_inventory.return_value
|
|
inventory["Inventory"]["batteries"] = [{"name": "Battery without serial"}]
|
|
|
|
await setup_integration(hass, mock_config_entry)
|
|
|
|
# Aggregate sensors are still created (battery exists in inventory)
|
|
charge_entry = entity_registry.async_get_entity_id(
|
|
"sensor", DOMAIN, f"{SITE_ID}_storage_charge_energy"
|
|
)
|
|
assert charge_entry is not None
|
|
|
|
# No per-battery sensors because the battery has no serial.
|
|
# Per-battery unique_ids follow `{site_id}_{serial}_battery_{key}`.
|
|
per_battery_entries = [
|
|
e
|
|
for e in er.async_entries_for_config_entry(
|
|
entity_registry, mock_config_entry.entry_id
|
|
)
|
|
if "_battery_" in e.unique_id
|
|
]
|
|
assert per_battery_entries == []
|
|
|
|
|
|
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
|
async def test_storage_service_not_retried_after_recovery_with_no_batteries(
|
|
recorder_mock: Recorder,
|
|
hass: HomeAssistant,
|
|
freezer: FrozenDateTimeFactory,
|
|
mock_config_entry: MockConfigEntry,
|
|
solaredge_api: Mock,
|
|
entity_registry: er.EntityRegistry,
|
|
) -> None:
|
|
"""Test storage stays idle when inventory has no batteries."""
|
|
solaredge_api.get_inventory.side_effect = KeyError("Inventory")
|
|
|
|
await setup_integration(hass, mock_config_entry)
|
|
|
|
# Inventory recovers but reports zero batteries.
|
|
solaredge_api.get_inventory.side_effect = None
|
|
solaredge_api.get_inventory.return_value = {"Inventory": {"batteries": []}}
|
|
|
|
freezer.tick(INVENTORY_UPDATE_DELAY)
|
|
async_fire_time_changed(hass)
|
|
await hass.async_block_till_done(wait_background_tasks=True)
|
|
|
|
charge_entry = entity_registry.async_get_entity_id(
|
|
"sensor", DOMAIN, f"{SITE_ID}_storage_charge_energy"
|
|
)
|
|
assert charge_entry is None
|