diff --git a/tests/components/solaredge/__init__.py b/tests/components/solaredge/__init__.py index c2a54cfafb62..010251296101 100644 --- a/tests/components/solaredge/__init__.py +++ b/tests/components/solaredge/__init__.py @@ -1 +1,14 @@ """Tests for the SolarEdge component.""" + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_integration( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Set up the SolarEdge integration in Home Assistant.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/solaredge/conftest.py b/tests/components/solaredge/conftest.py index 664716be1cf6..21cd8153d51c 100644 --- a/tests/components/solaredge/conftest.py +++ b/tests/components/solaredge/conftest.py @@ -5,6 +5,11 @@ from unittest.mock import AsyncMock, Mock, patch import pytest +from homeassistant.components.solaredge.const import CONF_SITE_ID, DOMAIN +from homeassistant.const import CONF_API_KEY, CONF_PASSWORD, CONF_USERNAME + +from tests.common import MockConfigEntry, load_json_object_fixture + SITE_ID = "1a2b3c4d5e6f7g8h" API_KEY = "a1b2c3d4e5f6g7h8" USERNAME = "test-username" @@ -21,11 +26,54 @@ def mock_setup_entry() -> Generator[AsyncMock]: yield mock_setup_entry +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return a MockConfigEntry with API key auth.""" + return MockConfigEntry( + domain=DOMAIN, + title="SolarEdge", + unique_id=SITE_ID, + data={CONF_SITE_ID: SITE_ID, CONF_API_KEY: API_KEY}, + ) + + +@pytest.fixture +def mock_config_entry_web_login() -> MockConfigEntry: + """Return a MockConfigEntry with web login auth.""" + return MockConfigEntry( + domain=DOMAIN, + title="SolarEdge", + unique_id=SITE_ID, + data={ + CONF_SITE_ID: SITE_ID, + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + }, + ) + + @pytest.fixture(name="solaredge_api") def mock_solaredge_api_fixture() -> Generator[Mock]: """Mock a successful SolarEdge Monitoring API.""" api = Mock() - api.get_details = AsyncMock(return_value={"details": {"status": "active"}}) + api.get_details = AsyncMock( + return_value=load_json_object_fixture("details.json", DOMAIN) + ) + api.get_overview = AsyncMock( + return_value=load_json_object_fixture("overview.json", DOMAIN) + ) + api.get_inventory = AsyncMock( + return_value=load_json_object_fixture("inventory.json", DOMAIN) + ) + api.get_current_power_flow = AsyncMock( + return_value=load_json_object_fixture("power_flow.json", DOMAIN) + ) + api.get_energy_details = AsyncMock( + return_value=load_json_object_fixture("energy_details.json", DOMAIN) + ) + api.get_storage_data = AsyncMock( + return_value=load_json_object_fixture("storage_data.json", DOMAIN) + ) with ( patch( "homeassistant.components.solaredge.config_flow.aiosolaredge.SolarEdge", diff --git a/tests/components/solaredge/fixtures/details.json b/tests/components/solaredge/fixtures/details.json new file mode 100644 index 000000000000..a17e31a27a0d --- /dev/null +++ b/tests/components/solaredge/fixtures/details.json @@ -0,0 +1,19 @@ +{ + "details": { + "id": 1, + "name": "Test Site", + "accountId": 1, + "status": "Active", + "peakPower": 5.5, + "lastUpdateTime": "2025-01-01", + "installationDate": "2024-01-01", + "ptoDate": null, + "notes": "", + "type": "Optimizers & Inverters", + "primaryModule": { + "manufacturerName": "TestManufacturer", + "modelName": "TestModel", + "maximumPower": 350.0 + } + } +} diff --git a/tests/components/solaredge/fixtures/energy_details.json b/tests/components/solaredge/fixtures/energy_details.json new file mode 100644 index 000000000000..6a66c810f395 --- /dev/null +++ b/tests/components/solaredge/fixtures/energy_details.json @@ -0,0 +1,28 @@ +{ + "energyDetails": { + "timeUnit": "DAY", + "unit": "Wh", + "meters": [ + { + "type": "Production", + "values": [{ "date": "2025-01-01 00:00:00", "value": 12345.0 }] + }, + { + "type": "Consumption", + "values": [{ "date": "2025-01-01 00:00:00", "value": 9876.0 }] + }, + { + "type": "SelfConsumption", + "values": [{ "date": "2025-01-01 00:00:00", "value": 5432.0 }] + }, + { + "type": "FeedIn", + "values": [{ "date": "2025-01-01 00:00:00", "value": 6913.0 }] + }, + { + "type": "Purchased", + "values": [{ "date": "2025-01-01 00:00:00", "value": 4444.0 }] + } + ] + } +} diff --git a/tests/components/solaredge/fixtures/inventory.json b/tests/components/solaredge/fixtures/inventory.json new file mode 100644 index 000000000000..971ec2e6ad5d --- /dev/null +++ b/tests/components/solaredge/fixtures/inventory.json @@ -0,0 +1,39 @@ +{ + "Inventory": { + "meters": [ + { "name": "Production meter", "manufacturer": "Acme", "model": "M1" }, + { "name": "Consumption meter", "manufacturer": "Acme", "model": "M1" } + ], + "sensors": [], + "gateways": [{ "name": "Gateway1", "serialNumber": "G-001" }], + "batteries": [ + { + "name": "Battery1", + "manufacturer": "Acme", + "model": "B1", + "SN": "BAT001", + "nameplateCapacity": 9700.0 + } + ], + "inverters": [ + { + "name": "Inverter1", + "manufacturer": "Acme", + "model": "I1", + "serialNumber": "INV-001" + }, + { + "name": "Inverter2", + "manufacturer": "Acme", + "model": "I1", + "serialNumber": "INV-002" + }, + { + "name": "Inverter3", + "manufacturer": "Acme", + "model": "I1", + "serialNumber": "INV-003" + } + ] + } +} diff --git a/tests/components/solaredge/fixtures/overview.json b/tests/components/solaredge/fixtures/overview.json new file mode 100644 index 000000000000..8e3c2ba0591c --- /dev/null +++ b/tests/components/solaredge/fixtures/overview.json @@ -0,0 +1,11 @@ +{ + "overview": { + "lastUpdateTime": "2025-01-01 12:00:00", + "lifeTimeData": { "energy": 100000.0, "revenue": 10.0 }, + "lastYearData": { "energy": 50000.0 }, + "lastMonthData": { "energy": 10000.0 }, + "lastDayData": { "energy": 250.0 }, + "currentPower": { "power": 1234.5 }, + "measuredBy": "INVERTER" + } +} diff --git a/tests/components/solaredge/fixtures/power_flow.json b/tests/components/solaredge/fixtures/power_flow.json new file mode 100644 index 000000000000..22c12c9d18a4 --- /dev/null +++ b/tests/components/solaredge/fixtures/power_flow.json @@ -0,0 +1,20 @@ +{ + "siteCurrentPowerFlow": { + "updateRefreshRate": 3, + "unit": "W", + "connections": [ + { "from": "GRID", "to": "Load" }, + { "from": "PV", "to": "Load" }, + { "from": "PV", "to": "STORAGE" } + ], + "GRID": { "status": "Active", "currentPower": 250.0 }, + "LOAD": { "status": "Active", "currentPower": 1500.0 }, + "PV": { "status": "Active", "currentPower": 1500.0 }, + "STORAGE": { + "status": "Charging", + "currentPower": 250.0, + "chargeLevel": 75, + "critical": false + } + } +} diff --git a/tests/components/solaredge/fixtures/storage_data.json b/tests/components/solaredge/fixtures/storage_data.json new file mode 100644 index 000000000000..21f406546713 --- /dev/null +++ b/tests/components/solaredge/fixtures/storage_data.json @@ -0,0 +1,25 @@ +{ + "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 + } + ] + } + ] + } +} diff --git a/tests/components/solaredge/snapshots/test_sensor.ambr b/tests/components/solaredge/snapshots/test_sensor.ambr new file mode 100644 index 000000000000..679a530201f3 --- /dev/null +++ b/tests/components/solaredge/snapshots/test_sensor.ambr @@ -0,0 +1,1582 @@ +# serializer version: 1 +# name: test_all_entities[sensor.battery_bat001_charge_energy_today-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.battery_bat001_charge_energy_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Charge energy today', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charge energy today', + 'platform': 'solaredge', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_charge_energy', + 'unique_id': '1a2b3c4d5e6f7g8h_BAT001_battery_charge_energy', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.battery_bat001_charge_energy_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Battery BAT001 Charge energy today', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.battery_bat001_charge_energy_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '500.0', + }) +# --- +# name: test_all_entities[sensor.battery_bat001_discharge_energy_today-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.battery_bat001_discharge_energy_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Discharge energy today', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Discharge energy today', + 'platform': 'solaredge', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_discharge_energy', + 'unique_id': '1a2b3c4d5e6f7g8h_BAT001_battery_discharge_energy', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.battery_bat001_discharge_energy_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Battery BAT001 Discharge energy today', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.battery_bat001_discharge_energy_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '300.0', + }) +# --- +# name: test_all_entities[sensor.battery_bat001_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.battery_bat001_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Power', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'solaredge', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_power', + 'unique_id': '1a2b3c4d5e6f7g8h_BAT001_battery_power', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.battery_bat001_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Battery BAT001 Power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.battery_bat001_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '200.0', + }) +# --- +# name: test_all_entities[sensor.battery_bat001_state_of_charge-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.battery_bat001_state_of_charge', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'State of charge', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'State of charge', + 'platform': 'solaredge', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'battery_state_of_charge', + 'unique_id': '1a2b3c4d5e6f7g8h_BAT001_battery_state_of_charge', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[sensor.battery_bat001_state_of_charge-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'Battery BAT001 State of charge', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.battery_bat001_state_of_charge', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '75.0', + }) +# --- +# name: test_all_entities[sensor.solaredge_batteries-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solaredge_batteries', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Batteries', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Batteries', + 'platform': 'solaredge', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'batteries', + 'unique_id': '1a2b3c4d5e6f7g8h_batteries', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.solaredge_batteries-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'batteries': list([ + dict({ + 'SN': 'BAT001', + 'manufacturer': 'Acme', + 'model': 'B1', + 'name': 'Battery1', + 'nameplateCapacity': 9700.0, + }), + ]), + 'friendly_name': 'SolarEdge Batteries', + }), + 'context': , + 'entity_id': 'sensor.solaredge_batteries', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1', + }) +# --- +# name: test_all_entities[sensor.solaredge_consumed_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solaredge_consumed_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Consumed energy', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Consumed energy', + 'platform': 'solaredge', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'consumption_energy', + 'unique_id': '1a2b3c4d5e6f7g8h_consumption_energy', + 'unit_of_measurement': 'Wh', + }) +# --- +# name: test_all_entities[sensor.solaredge_consumed_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'date': '2025-01-01 00:00:00', + 'device_class': 'energy', + 'friendly_name': 'SolarEdge Consumed energy', + 'state_class': , + 'unit_of_measurement': 'Wh', + }), + 'context': , + 'entity_id': 'sensor.solaredge_consumed_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '9876.0', + }) +# --- +# name: test_all_entities[sensor.solaredge_current_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solaredge_current_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Current power', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Current power', + 'platform': 'solaredge', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'current_power', + 'unique_id': '1a2b3c4d5e6f7g8h_current_power', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solaredge_current_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'SolarEdge Current power', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solaredge_current_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1234.5', + }) +# --- +# name: test_all_entities[sensor.solaredge_energy_this_month-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solaredge_energy_this_month', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Energy this month', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy this month', + 'platform': 'solaredge', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'energy_this_month', + 'unique_id': '1a2b3c4d5e6f7g8h_energy_this_month', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solaredge_energy_this_month-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'SolarEdge Energy this month', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solaredge_energy_this_month', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10000.0', + }) +# --- +# name: test_all_entities[sensor.solaredge_energy_this_year-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solaredge_energy_this_year', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Energy this year', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy this year', + 'platform': 'solaredge', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'energy_this_year', + 'unique_id': '1a2b3c4d5e6f7g8h_energy_this_year', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solaredge_energy_this_year-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'SolarEdge Energy this year', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solaredge_energy_this_year', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '50000.0', + }) +# --- +# name: test_all_entities[sensor.solaredge_energy_today-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solaredge_energy_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Energy today', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy today', + 'platform': 'solaredge', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'energy_today', + 'unique_id': '1a2b3c4d5e6f7g8h_energy_today', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solaredge_energy_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'SolarEdge Energy today', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solaredge_energy_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '250.0', + }) +# --- +# name: test_all_entities[sensor.solaredge_exported_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solaredge_exported_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Exported energy', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Exported energy', + 'platform': 'solaredge', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'feedin_energy', + 'unique_id': '1a2b3c4d5e6f7g8h_feedin_energy', + 'unit_of_measurement': 'Wh', + }) +# --- +# name: test_all_entities[sensor.solaredge_exported_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'date': '2025-01-01 00:00:00', + 'device_class': 'energy', + 'friendly_name': 'SolarEdge Exported energy', + 'state_class': , + 'unit_of_measurement': 'Wh', + }), + 'context': , + 'entity_id': 'sensor.solaredge_exported_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '6913.0', + }) +# --- +# name: test_all_entities[sensor.solaredge_gateways-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solaredge_gateways', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Gateways', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Gateways', + 'platform': 'solaredge', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'gateways', + 'unique_id': '1a2b3c4d5e6f7g8h_gateways', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.solaredge_gateways-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'SolarEdge Gateways', + 'gateways': list([ + dict({ + 'name': 'Gateway1', + 'serialNumber': 'G-001', + }), + ]), + }), + 'context': , + 'entity_id': 'sensor.solaredge_gateways', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1', + }) +# --- +# name: test_all_entities[sensor.solaredge_grid_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solaredge_grid_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Grid power', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Grid power', + 'platform': 'solaredge', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'grid_power', + 'unique_id': '1a2b3c4d5e6f7g8h_grid_power', + 'unit_of_measurement': 'W', + }) +# --- +# name: test_all_entities[sensor.solaredge_grid_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'flow': 'import', + 'friendly_name': 'SolarEdge Grid power', + 'state_class': , + 'status': 'Active', + 'unit_of_measurement': 'W', + }), + 'context': , + 'entity_id': 'sensor.solaredge_grid_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '250.0', + }) +# --- +# name: test_all_entities[sensor.solaredge_imported_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solaredge_imported_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Imported energy', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Imported energy', + 'platform': 'solaredge', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'purchased_energy', + 'unique_id': '1a2b3c4d5e6f7g8h_purchased_energy', + 'unit_of_measurement': 'Wh', + }) +# --- +# name: test_all_entities[sensor.solaredge_imported_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'date': '2025-01-01 00:00:00', + 'device_class': 'energy', + 'friendly_name': 'SolarEdge Imported energy', + 'state_class': , + 'unit_of_measurement': 'Wh', + }), + 'context': , + 'entity_id': 'sensor.solaredge_imported_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4444.0', + }) +# --- +# name: test_all_entities[sensor.solaredge_inverters-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solaredge_inverters', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Inverters', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Inverters', + 'platform': 'solaredge', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'inverters', + 'unique_id': '1a2b3c4d5e6f7g8h_inverters', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.solaredge_inverters-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'SolarEdge Inverters', + 'inverters': list([ + dict({ + 'manufacturer': 'Acme', + 'model': 'I1', + 'name': 'Inverter1', + 'serialNumber': 'INV-001', + }), + dict({ + 'manufacturer': 'Acme', + 'model': 'I1', + 'name': 'Inverter2', + 'serialNumber': 'INV-002', + }), + dict({ + 'manufacturer': 'Acme', + 'model': 'I1', + 'name': 'Inverter3', + 'serialNumber': 'INV-003', + }), + ]), + }), + 'context': , + 'entity_id': 'sensor.solaredge_inverters', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3', + }) +# --- +# name: test_all_entities[sensor.solaredge_lifetime_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solaredge_lifetime_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Lifetime energy', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Lifetime energy', + 'platform': 'solaredge', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'lifetime_energy', + 'unique_id': '1a2b3c4d5e6f7g8h_lifetime_energy', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solaredge_lifetime_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'SolarEdge Lifetime energy', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solaredge_lifetime_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100000.0', + }) +# --- +# name: test_all_entities[sensor.solaredge_meters-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solaredge_meters', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Meters', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Meters', + 'platform': 'solaredge', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'meters', + 'unique_id': '1a2b3c4d5e6f7g8h_meters', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.solaredge_meters-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'SolarEdge Meters', + 'meters': list([ + dict({ + 'manufacturer': 'Acme', + 'model': 'M1', + 'name': 'Production meter', + }), + dict({ + 'manufacturer': 'Acme', + 'model': 'M1', + 'name': 'Consumption meter', + }), + ]), + }), + 'context': , + 'entity_id': 'sensor.solaredge_meters', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2', + }) +# --- +# name: test_all_entities[sensor.solaredge_power_consumption-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solaredge_power_consumption', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Power consumption', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power consumption', + 'platform': 'solaredge', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'power_consumption', + 'unique_id': '1a2b3c4d5e6f7g8h_power_consumption', + 'unit_of_measurement': 'W', + }) +# --- +# name: test_all_entities[sensor.solaredge_power_consumption-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'SolarEdge Power consumption', + 'state_class': , + 'status': 'Active', + 'unit_of_measurement': 'W', + }), + 'context': , + 'entity_id': 'sensor.solaredge_power_consumption', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1500.0', + }) +# --- +# name: test_all_entities[sensor.solaredge_produced_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solaredge_produced_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Produced energy', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Produced energy', + 'platform': 'solaredge', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'production_energy', + 'unique_id': '1a2b3c4d5e6f7g8h_production_energy', + 'unit_of_measurement': 'Wh', + }) +# --- +# name: test_all_entities[sensor.solaredge_produced_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'date': '2025-01-01 00:00:00', + 'device_class': 'energy', + 'friendly_name': 'SolarEdge Produced energy', + 'state_class': , + 'unit_of_measurement': 'Wh', + }), + 'context': , + 'entity_id': 'sensor.solaredge_produced_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '12345.0', + }) +# --- +# name: test_all_entities[sensor.solaredge_self_consumed_energy-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solaredge_self_consumed_energy', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Self-consumed energy', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Self-consumed energy', + 'platform': 'solaredge', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'selfconsumption_energy', + 'unique_id': '1a2b3c4d5e6f7g8h_selfconsumption_energy', + 'unit_of_measurement': 'Wh', + }) +# --- +# name: test_all_entities[sensor.solaredge_self_consumed_energy-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'date': '2025-01-01 00:00:00', + 'device_class': 'energy', + 'friendly_name': 'SolarEdge Self-consumed energy', + 'state_class': , + 'unit_of_measurement': 'Wh', + }), + 'context': , + 'entity_id': 'sensor.solaredge_self_consumed_energy', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5432.0', + }) +# --- +# name: test_all_entities[sensor.solaredge_sensors-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solaredge_sensors', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Sensors', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Sensors', + 'platform': 'solaredge', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'sensors', + 'unique_id': '1a2b3c4d5e6f7g8h_sensors', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.solaredge_sensors-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'SolarEdge Sensors', + 'sensors': list([ + ]), + }), + 'context': , + 'entity_id': 'sensor.solaredge_sensors', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_all_entities[sensor.solaredge_site_details-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solaredge_site_details', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Site details', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Site details', + 'platform': 'solaredge', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'site_details', + 'unique_id': '1a2b3c4d5e6f7g8h', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[sensor.solaredge_site_details-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'SolarEdge Site details', + 'installation_date': '2024-01-01', + 'last_update_time': '2025-01-01', + 'manufacturer_name': 'TestManufacturer', + 'maximum_power': 350.0, + 'model_name': 'TestModel', + 'name': 'Test Site', + 'peak_power': 5.5, + 'type': 'Optimizers & Inverters', + }), + 'context': , + 'entity_id': 'sensor.solaredge_site_details', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'Active', + }) +# --- +# name: test_all_entities[sensor.solaredge_solar_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solaredge_solar_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Solar power', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Solar power', + 'platform': 'solaredge', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'solar_power', + 'unique_id': '1a2b3c4d5e6f7g8h_solar_power', + 'unit_of_measurement': 'W', + }) +# --- +# name: test_all_entities[sensor.solaredge_solar_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'SolarEdge Solar power', + 'state_class': , + 'status': 'Active', + 'unit_of_measurement': 'W', + }), + 'context': , + 'entity_id': 'sensor.solaredge_solar_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '1500.0', + }) +# --- +# name: test_all_entities[sensor.solaredge_storage_charge_energy_today-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solaredge_storage_charge_energy_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Storage charge energy today', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Storage charge energy today', + 'platform': 'solaredge', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'storage_charge_energy', + 'unique_id': '1a2b3c4d5e6f7g8h_storage_charge_energy', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solaredge_storage_charge_energy_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'SolarEdge Storage charge energy today', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solaredge_storage_charge_energy_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '500.0', + }) +# --- +# name: test_all_entities[sensor.solaredge_storage_discharge_energy_today-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solaredge_storage_discharge_energy_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Storage discharge energy today', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Storage discharge energy today', + 'platform': 'solaredge', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'storage_discharge_energy', + 'unique_id': '1a2b3c4d5e6f7g8h_storage_discharge_energy', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.solaredge_storage_discharge_energy_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'SolarEdge Storage discharge energy today', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solaredge_storage_discharge_energy_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '300.0', + }) +# --- +# name: test_all_entities[sensor.solaredge_storage_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solaredge_storage_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Storage level', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Storage level', + 'platform': 'solaredge', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'storage_level', + 'unique_id': '1a2b3c4d5e6f7g8h_storage_level', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[sensor.solaredge_storage_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'battery', + 'friendly_name': 'SolarEdge Storage level', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.solaredge_storage_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '75', + }) +# --- +# name: test_all_entities[sensor.solaredge_storage_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solaredge_storage_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Storage power', + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Storage power', + 'platform': 'solaredge', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'storage_power', + 'unique_id': '1a2b3c4d5e6f7g8h_storage_power', + 'unit_of_measurement': 'W', + }) +# --- +# name: test_all_entities[sensor.solaredge_storage_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'flow': 'charge', + 'friendly_name': 'SolarEdge Storage power', + 'soc': 75, + 'state_class': , + 'status': 'Charging', + 'unit_of_measurement': 'W', + }), + 'context': , + 'entity_id': 'sensor.solaredge_storage_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '-250.0', + }) +# --- diff --git a/tests/components/solaredge/test_coordinator.py b/tests/components/solaredge/test_coordinator.py index 982ccad2c92b..c385e5d28d12 100644 --- a/tests/components/solaredge/test_coordinator.py +++ b/tests/components/solaredge/test_coordinator.py @@ -4,6 +4,7 @@ import asyncio from datetime import datetime, timedelta from unittest.mock import AsyncMock, MagicMock, patch +from aiohttp import ClientError from freezegun.api import FrozenDateTimeFactory import pytest from solaredge_web import EnergyData @@ -18,6 +19,7 @@ from homeassistant.components.solaredge.const import ( OVERVIEW_UPDATE_DELAY, ) from homeassistant.components.solaredge.coordinator import SolarEdgeModulesCoordinator +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( CONF_API_KEY, CONF_NAME, @@ -28,6 +30,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util +from . import setup_integration from .conftest import API_KEY, PASSWORD, SITE_ID, USERNAME from tests.common import MockConfigEntry, async_fire_time_changed @@ -417,3 +420,17 @@ async def test_modules_coordinator_no_energy_data( {"state", "sum"}, ) assert not stats + + +async def test_modules_coordinator_api_failure( + recorder_mock: Recorder, + hass: HomeAssistant, + mock_config_entry_web_login: MockConfigEntry, + solaredge_web_api: AsyncMock, +) -> None: + """Test the modules coordinator surfaces API failures via the config entry state.""" + solaredge_web_api.async_get_equipment.side_effect = ClientError("boom") + + await setup_integration(hass, mock_config_entry_web_login) + + assert mock_config_entry_web_login.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/solaredge/test_init.py b/tests/components/solaredge/test_init.py index fca1bb7876b3..6901a46ba520 100644 --- a/tests/components/solaredge/test_init.py +++ b/tests/components/solaredge/test_init.py @@ -3,6 +3,7 @@ from unittest.mock import AsyncMock, Mock, patch from aiohttp import ClientError +import pytest from homeassistant.components.recorder import Recorder from homeassistant.components.solaredge.const import CONF_SITE_ID, DOMAIN @@ -10,6 +11,7 @@ from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_API_KEY, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from . import setup_integration from .conftest import API_KEY, PASSWORD, SITE_ID, USERNAME from tests.common import MockConfigEntry @@ -156,3 +158,29 @@ async def test_web_login_config_not_ready( await hass.async_block_till_done() assert entry.state is ConfigEntryState.SETUP_RETRY + + +@pytest.mark.parametrize( + ("get_details_response", "expected_state"), + [ + # Missing 'details' key → ConfigEntryNotReady → SETUP_RETRY + ({}, ConfigEntryState.SETUP_RETRY), + # Site status is not 'active' → setup returns False → SETUP_ERROR + ({"details": {"status": "Disabled"}}, ConfigEntryState.SETUP_ERROR), + ], + ids=["missing_details_key", "site_not_active"], +) +async def test_setup_api_key_failure( + recorder_mock: Recorder, + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + solaredge_api: Mock, + get_details_response: dict, + expected_state: ConfigEntryState, +) -> None: + """Test the API-key setup failure paths in async_setup_entry.""" + solaredge_api.get_details.return_value = get_details_response + + await setup_integration(hass, mock_config_entry) + + assert mock_config_entry.state is expected_state diff --git a/tests/components/solaredge/test_sensor.py b/tests/components/solaredge/test_sensor.py index 0dd514d0e80b..2e1527f6ba5a 100644 --- a/tests/components/solaredge/test_sensor.py +++ b/tests/components/solaredge/test_sensor.py @@ -1,100 +1,22 @@ -"""Tests for the SolarEdge sensors.""" +"""Tests for the SolarEdge sensor platform.""" -from unittest.mock import AsyncMock, MagicMock, patch +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 ( - CONF_SITE_ID, - DEFAULT_NAME, - DOMAIN, - INVENTORY_UPDATE_DELAY, -) -from homeassistant.const import CONF_API_KEY, CONF_NAME +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 .conftest import API_KEY, SITE_ID +from . import setup_integration +from .conftest import 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, - }, - ], - } - ] - } -} +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform STORAGE_DATA_MULTI_BATTERY = { "storageData": { @@ -142,22 +64,285 @@ STORAGE_DATA_MULTI_BATTERY = { } -@patch("homeassistant.components.solaredge.SolarEdge") -async def test_storage_data_service( - mock_solaredge: MagicMock, +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_all_entities( recorder_mock: Recorder, hass: HomeAssistant, - freezer: FrozenDateTimeFactory, - mock_solaredge_api: AsyncMock, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, mock_config_entry: MockConfigEntry, + solaredge_api: Mock, +) -> None: + """Test all sensor entities are created with the correct state and registry entry.""" + 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 (unknown) when site has no storage in flow data.""" + 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 when a data service refresh raises 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 data service skips meters without type/values and unsupported types.""" + 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 reports for storage discharge.""" + 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.""" - 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() + await setup_integration(hass, mock_config_entry) # Aggregate sensors charge_entry = entity_registry.async_get_entity_id( @@ -177,7 +362,7 @@ async def test_storage_data_service( assert state is not None assert float(state.state) == 300.0 # 800 - 500 - # Per-battery entities + # Per-battery entities for BAT001 bat_charge = entity_registry.async_get_entity_id( "sensor", DOMAIN, f"{SITE_ID}_BAT001_battery_charge_energy" ) @@ -212,28 +397,20 @@ async def test_storage_data_service( assert float(state.state) == 200.0 -@patch("homeassistant.components.solaredge.SolarEdge") +@pytest.mark.usefixtures("entity_registry_enabled_by_default") 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, + solaredge_api: Mock, 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 + 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 - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() + await setup_integration(hass, mock_config_entry) charge_entry = entity_registry.async_get_entity_id( "sensor", DOMAIN, f"{SITE_ID}_storage_charge_energy" @@ -246,24 +423,15 @@ async def test_storage_data_service_multi_battery( # 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 + assert float(hass.states.get(charge_entry).state) == 1200.0 + assert float(hass.states.get(discharge_entry).state) == 700.0 - 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 + assert float(hass.states.get(bat1_soc).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" ) @@ -272,37 +440,23 @@ async def test_storage_data_service_multi_battery( ) 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 + assert float(hass.states.get(bat2_charge).state) == 700.0 + assert float(hass.states.get(bat2_soc).state) == 80.0 -@patch("homeassistant.components.solaredge.SolarEdge") -async def test_storage_data_service_no_batteries( - mock_solaredge: MagicMock, +async def test_storage_service_not_created_when_inventory_has_no_batteries( recorder_mock: Recorder, hass: HomeAssistant, - freezer: FrozenDateTimeFactory, - mock_solaredge_api: AsyncMock, mock_config_entry: MockConfigEntry, + solaredge_api: Mock, 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 + inventory = solaredge_api.get_inventory.return_value + inventory["Inventory"]["batteries"] = [] - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() + await setup_integration(hass, mock_config_entry) - # 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" ) @@ -313,23 +467,18 @@ async def test_storage_data_service_no_batteries( assert discharge_entry is None -@patch("homeassistant.components.solaredge.SolarEdge") +@pytest.mark.usefixtures("entity_registry_enabled_by_default") 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, + solaredge_api: Mock, 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 + """Test storage sensors are unavailable when the storage API errors out.""" + solaredge_api.get_storage_data.side_effect = Exception("API error") - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() + await setup_integration(hass, mock_config_entry) charge_entry = entity_registry.async_get_entity_id( "sensor", DOMAIN, f"{SITE_ID}_storage_charge_energy" @@ -340,34 +489,28 @@ async def test_storage_data_service_api_error( 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" + assert hass.states.get(charge_entry).state == STATE_UNAVAILABLE + assert hass.states.get(discharge_entry).state == STATE_UNAVAILABLE -@patch("homeassistant.components.solaredge.SolarEdge") +@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( - mock_solaredge: MagicMock, recorder_mock: Recorder, hass: HomeAssistant, - freezer: FrozenDateTimeFactory, - mock_solaredge_api: AsyncMock, mock_config_entry: MockConfigEntry, + solaredge_api: Mock, entity_registry: er.EntityRegistry, + bad_response: dict, ) -> 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 + """Test storage sensors are unavailable when the response is missing required keys.""" + solaredge_api.get_storage_data.return_value = bad_response - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() + await setup_integration(hass, mock_config_entry) charge_entry = entity_registry.async_get_entity_id( "sensor", DOMAIN, f"{SITE_ID}_storage_charge_energy" @@ -377,126 +520,181 @@ async def test_storage_data_missing_keys_in_response( ) 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" + assert hass.states.get(charge_entry).state == STATE_UNAVAILABLE + assert hass.states.get(discharge_entry).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") +@pytest.mark.usefixtures("entity_registry_enabled_by_default") 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, + solaredge_api: Mock, 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 + """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") - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() + await setup_integration(hass, mock_config_entry) - # 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 + # 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 ) - # Trigger inventory coordinator refresh + # 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) - # Storage sensors should now exist + 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 data service tolerates batteries without serial / telemetries / single telemetry.""" + 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" ) - 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 state.state == expected_charge_state -@patch("homeassistant.components.solaredge.SolarEdge") -async def test_storage_service_not_created_when_inventory_has_no_batteries( - mock_solaredge: MagicMock, +@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 inventory batteries without a serial number 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_solaredge_api: AsyncMock, mock_config_entry: MockConfigEntry, + solaredge_api: Mock, 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 + """Test storage service stays idle when inventory recovers but reports no batteries.""" + solaredge_api.get_inventory.side_effect = KeyError("Inventory") - mock_config_entry.add_to_hass(hass) - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() + await setup_integration(hass, mock_config_entry) - # Inventory recovers but reports zero batteries - mock_solaredge_api.get_inventory = AsyncMock( - return_value={"Inventory": {"batteries": []}} - ) + # 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) - # Storage sensors should still not exist charge_entry = entity_registry.async_get_entity_id( "sensor", DOMAIN, f"{SITE_ID}_storage_charge_energy" )