1
0
mirror of https://github.com/home-assistant/core.git synced 2026-05-15 13:01:22 +01:00
Files
core/tests/components/solaredge/test_sensor.py
T

504 lines
17 KiB
Python

"""Tests for the SolarEdge sensors."""
from unittest.mock import AsyncMock, MagicMock, patch
from freezegun.api import FrozenDateTimeFactory
import pytest
from homeassistant.components.recorder import Recorder
from homeassistant.components.solaredge.const import (
CONF_SITE_ID,
DEFAULT_NAME,
DOMAIN,
INVENTORY_UPDATE_DELAY,
)
from homeassistant.const import CONF_API_KEY, CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from .conftest import API_KEY, SITE_ID
from tests.common import MockConfigEntry, async_fire_time_changed
@pytest.fixture(autouse=True)
def enable_all_entities(entity_registry_enabled_by_default: None) -> None:
"""Make sure all entities are enabled."""
@pytest.fixture
def mock_solaredge_api() -> AsyncMock:
"""Return a mocked SolarEdge API with common defaults."""
api = AsyncMock()
api.get_details = AsyncMock(return_value={"details": {"status": "active"}})
api.get_overview = AsyncMock(
return_value={
"overview": {
"lifeTimeData": {"energy": 100000},
"lastYearData": {"energy": 50000},
"lastMonthData": {"energy": 10000},
"lastDayData": {"energy": 0.0},
"currentPower": {"power": 0.0},
}
}
)
api.get_inventory = AsyncMock(
return_value={"Inventory": {"batteries": [{"SN": "BAT001"}]}}
)
api.get_current_power_flow = AsyncMock(
return_value={
"siteCurrentPowerFlow": {
"unit": "W",
"connections": [],
}
}
)
api.get_energy_details = AsyncMock(
return_value={"energyDetails": {"unit": "Wh", "meters": []}}
)
api.get_storage_data = AsyncMock(return_value=STORAGE_DATA_SINGLE_BATTERY)
return api
@pytest.fixture
def mock_config_entry() -> MockConfigEntry:
"""Return a default mocked config entry for storage tests."""
return MockConfigEntry(
domain=DOMAIN,
title=DEFAULT_NAME,
data={CONF_NAME: DEFAULT_NAME, CONF_SITE_ID: SITE_ID, CONF_API_KEY: API_KEY},
)
STORAGE_DATA_SINGLE_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,
},
],
}
]
}
}
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,
},
],
},
]
}
}
@patch("homeassistant.components.solaredge.SolarEdge")
async def test_storage_data_service(
mock_solaredge: MagicMock,
recorder_mock: Recorder,
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_solaredge_api: AsyncMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test storage data service fetches battery charge/discharge energy."""
mock_solaredge.return_value = mock_solaredge_api
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
# 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
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
@patch("homeassistant.components.solaredge.SolarEdge")
async def test_storage_data_service_multi_battery(
mock_solaredge: MagicMock,
recorder_mock: Recorder,
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_solaredge_api: AsyncMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test storage data service aggregates data across multiple batteries."""
mock_solaredge_api.get_inventory = AsyncMock(
return_value={"Inventory": {"batteries": [{"SN": "BAT001"}, {"SN": "BAT002"}]}}
)
mock_solaredge_api.get_storage_data = AsyncMock(
return_value=STORAGE_DATA_MULTI_BATTERY
)
mock_solaredge.return_value = mock_solaredge_api
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
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)
state = hass.states.get(charge_entry)
assert state is not None
assert float(state.state) == 1200.0 # 500 + 700
state = hass.states.get(discharge_entry)
assert state is not None
assert float(state.state) == 700.0 # 300 + 400
# Per-battery entities for BAT001
bat1_soc = entity_registry.async_get_entity_id(
"sensor", DOMAIN, f"{SITE_ID}_BAT001_battery_state_of_charge"
)
assert bat1_soc is not None
state = hass.states.get(bat1_soc)
assert state is not None
assert float(state.state) == 75.0
# Per-battery entities for BAT002
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
state = hass.states.get(bat2_charge)
assert state is not None
assert float(state.state) == 700.0
state = hass.states.get(bat2_soc)
assert state is not None
assert float(state.state) == 80.0
@patch("homeassistant.components.solaredge.SolarEdge")
async def test_storage_data_service_no_batteries(
mock_solaredge: MagicMock,
recorder_mock: Recorder,
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_solaredge_api: AsyncMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test storage service is not created when no batteries in inventory."""
mock_solaredge_api.get_inventory = AsyncMock(
return_value={"Inventory": {"batteries": []}}
)
mock_solaredge.return_value = mock_solaredge_api
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
# Sensors should not exist when inventory reports no batteries
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
@patch("homeassistant.components.solaredge.SolarEdge")
async def test_storage_data_service_api_error(
mock_solaredge: MagicMock,
recorder_mock: Recorder,
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_solaredge_api: AsyncMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test storage data service handles API errors gracefully."""
mock_solaredge_api.get_storage_data = AsyncMock(side_effect=Exception("API error"))
mock_solaredge.return_value = mock_solaredge_api
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
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
# Sensors should be unavailable when the API returns an error
state = hass.states.get(charge_entry)
assert state is not None
assert state.state == "unavailable"
state = hass.states.get(discharge_entry)
assert state is not None
assert state.state == "unavailable"
@patch("homeassistant.components.solaredge.SolarEdge")
async def test_storage_data_missing_keys_in_response(
mock_solaredge: MagicMock,
recorder_mock: Recorder,
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_solaredge_api: AsyncMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test storage service raises UpdateFailed when response is missing required keys."""
# API returns a response but without the storageData key
mock_solaredge_api.get_storage_data = AsyncMock(return_value={"unexpected": {}})
mock_solaredge.return_value = mock_solaredge_api
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
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
# Sensors should be unavailable due to UpdateFailed from missing key
state = hass.states.get(charge_entry)
assert state is not None
assert state.state == "unavailable"
state = hass.states.get(discharge_entry)
assert state is not None
assert state.state == "unavailable"
@patch("homeassistant.components.solaredge.SolarEdge")
async def test_storage_data_missing_batteries_key(
mock_solaredge: MagicMock,
recorder_mock: Recorder,
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_solaredge_api: AsyncMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test storage service raises UpdateFailed when batteries key is missing."""
# API returns storageData but without batteries key
mock_solaredge_api.get_storage_data = AsyncMock(
return_value={"storageData": {"otherField": "value"}}
)
mock_solaredge.return_value = mock_solaredge_api
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
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 == "unavailable"
@patch("homeassistant.components.solaredge.SolarEdge")
async def test_storage_service_deferred_after_inventory_failure(
mock_solaredge: MagicMock,
recorder_mock: Recorder,
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_solaredge_api: AsyncMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test storage service is created after inventory recovers from failure."""
# Initial inventory fetch fails
mock_solaredge_api.get_inventory = AsyncMock(side_effect=KeyError("Inventory"))
mock_solaredge.return_value = mock_solaredge_api
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
# Storage sensors should not exist yet
charge_entry = entity_registry.async_get_entity_id(
"sensor", DOMAIN, f"{SITE_ID}_storage_charge_energy"
)
assert charge_entry is None
# Now inventory recovers and reports batteries
mock_solaredge_api.get_inventory = AsyncMock(
return_value={"Inventory": {"batteries": [{"SN": "BAT001"}]}}
)
mock_solaredge_api.get_storage_data = AsyncMock(
return_value=STORAGE_DATA_SINGLE_BATTERY
)
# Trigger inventory coordinator refresh
freezer.tick(INVENTORY_UPDATE_DELAY)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
# Storage sensors should now exist
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
@patch("homeassistant.components.solaredge.SolarEdge")
async def test_storage_service_not_created_when_inventory_has_no_batteries(
mock_solaredge: MagicMock,
recorder_mock: Recorder,
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_solaredge_api: AsyncMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test storage service is not retried when inventory succeeds with no batteries."""
# Initial inventory fails
mock_solaredge_api.get_inventory = AsyncMock(side_effect=KeyError("Inventory"))
mock_solaredge.return_value = mock_solaredge_api
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
# Inventory recovers but reports zero batteries
mock_solaredge_api.get_inventory = AsyncMock(
return_value={"Inventory": {"batteries": []}}
)
freezer.tick(INVENTORY_UPDATE_DELAY)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
# Storage sensors should still not exist
charge_entry = entity_registry.async_get_entity_id(
"sensor", DOMAIN, f"{SITE_ID}_storage_charge_energy"
)
assert charge_entry is None