diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 549dc1b234184c..1ddc7654d62b41 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -71,6 +71,7 @@ async_get_media_source_devices, async_get_transcoder, ) +from .services import async_setup_services from .types import DevicesAddedListener, NestConfigEntry, NestData _LOGGER = logging.getLogger(__name__) @@ -115,6 +116,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up Nest components with dispatch between old/new flows.""" hass.http.register_view(NestEventMediaView(hass)) hass.http.register_view(NestEventMediaThumbnailView(hass)) + async_setup_services(hass) return True diff --git a/homeassistant/components/nest/climate.py b/homeassistant/components/nest/climate.py index b2ca81563f7f2f..ea4073f59685e1 100644 --- a/homeassistant/components/nest/climate.py +++ b/homeassistant/components/nest/climate.py @@ -1,5 +1,6 @@ """Support for Google Nest SDM climate devices.""" +from datetime import timedelta from typing import Any, cast from google_nest_sdm.device import Device @@ -67,7 +68,7 @@ FAN_INV_MODE_MAP = {v: k for k, v in FAN_MODE_MAP.items()} FAN_INV_MODES = list(FAN_INV_MODE_MAP) -MAX_FAN_DURATION = 43200 # 15 hours is the max in the SDM API +MAX_FAN_DURATION = 43200 # 12 hours is the max in the SDM API MIN_TEMP = 10 MAX_TEMP = 32 MIN_TEMP_RANGE = 1.66667 @@ -344,3 +345,27 @@ async def async_set_fan_mode(self, fan_mode: str) -> None: raise HomeAssistantError( f"Error setting {self.entity_id} fan mode to {fan_mode}: {err}" ) from err + + async def async_set_fan_timer(self, duration: timedelta) -> None: + """Set a short term fan timer.""" + if not self.supported_features & ClimateEntityFeature.FAN_MODE: + raise HomeAssistantError(f"Entity {self.entity_id} does not support fan") + + if self.hvac_mode == HVACMode.OFF: + raise HomeAssistantError( + f"Cannot turn on fan for {self.entity_id}, please set an HVAC mode (e.g. heat/cool) first" + ) + + seconds = int(duration.total_seconds()) + if seconds <= 0 or seconds > MAX_FAN_DURATION: + raise ValueError( + f"Duration {seconds} for {self.entity_id} must be between 1 and {MAX_FAN_DURATION} seconds" + ) + + trait = self._device.traits[FanTrait.NAME] + try: + await trait.set_timer(FAN_INV_MODE_MAP[FAN_ON], duration=seconds) + except ApiException as err: + raise HomeAssistantError( + f"Error setting {self.entity_id} fan timer: {err}" + ) from err diff --git a/homeassistant/components/nest/icons.json b/homeassistant/components/nest/icons.json new file mode 100644 index 00000000000000..1147328d12a4a6 --- /dev/null +++ b/homeassistant/components/nest/icons.json @@ -0,0 +1,7 @@ +{ + "services": { + "set_fan_timer": { + "service": "mdi:fan-clock" + } + } +} diff --git a/homeassistant/components/nest/services.py b/homeassistant/components/nest/services.py new file mode 100644 index 00000000000000..0cb97f5a0a541d --- /dev/null +++ b/homeassistant/components/nest/services.py @@ -0,0 +1,30 @@ +"""Define services for the Nest integration.""" + +import voluptuous as vol + +from homeassistant.components.climate import ( + DOMAIN as CLIMATE_DOMAIN, + ClimateEntityFeature, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import config_validation as cv, service + +from .const import DOMAIN + +SERVICE_SET_FAN_TIMER = "set_fan_timer" + + +@callback +def async_setup_services(hass: HomeAssistant) -> None: + """Register services for the Nest integration.""" + service.async_register_platform_entity_service( + hass, + DOMAIN, + SERVICE_SET_FAN_TIMER, + entity_domain=CLIMATE_DOMAIN, + schema={ + vol.Required("duration"): cv.time_period, + }, + func="async_set_fan_timer", + required_features=[ClimateEntityFeature.FAN_MODE], + ) diff --git a/homeassistant/components/nest/services.yaml b/homeassistant/components/nest/services.yaml new file mode 100644 index 00000000000000..820ed5e1779230 --- /dev/null +++ b/homeassistant/components/nest/services.yaml @@ -0,0 +1,12 @@ +set_fan_timer: + target: + entity: + domain: climate + integration: nest + supported_features: + - climate.ClimateEntityFeature.FAN_MODE + fields: + duration: + required: true + selector: + duration: diff --git a/homeassistant/components/nest/strings.json b/homeassistant/components/nest/strings.json index aa4490d03f92c0..6ae0d6622b543e 100644 --- a/homeassistant/components/nest/strings.json +++ b/homeassistant/components/nest/strings.json @@ -163,5 +163,17 @@ "create_new_topic": "Create new topic" } } + }, + "services": { + "set_fan_timer": { + "description": "Sets the fan to run for a specific duration.", + "fields": { + "duration": { + "description": "The duration the fan should run for.", + "name": "Duration" + } + }, + "name": "Set fan timer" + } } } diff --git a/homeassistant/components/weatherflow_cloud/sensor.py b/homeassistant/components/weatherflow_cloud/sensor.py index be4fd2ee9a2249..51c5839b41507d 100644 --- a/homeassistant/components/weatherflow_cloud/sensor.py +++ b/homeassistant/components/weatherflow_cloud/sensor.py @@ -485,9 +485,22 @@ class WeatherFlowCloudSensorREST(WeatherFlowSensorBase): coordinator: WeatherFlowCloudUpdateCoordinatorREST + @property + def _observation(self) -> Observation | None: + """Return the current station observation.""" + observations = self.coordinator.data[self.station_id].observation.obs + if not observations: + return None + return observations[0] + + @property + def available(self) -> bool: + """Get if available.""" + return super().available and self._observation is not None + @property def native_value(self) -> StateType | datetime: """Return the native value.""" - return self.entity_description.value_fn( - self.coordinator.data[self.station_id].observation.obs[0] - ) + if (observation := self._observation) is None: + return None + return self.entity_description.value_fn(observation) diff --git a/homeassistant/components/wmspro/manifest.json b/homeassistant/components/wmspro/manifest.json index 5faf2c4ed79453..ae83d79a736178 100644 --- a/homeassistant/components/wmspro/manifest.json +++ b/homeassistant/components/wmspro/manifest.json @@ -14,5 +14,5 @@ "documentation": "https://www.home-assistant.io/integrations/wmspro", "integration_type": "hub", "iot_class": "local_polling", - "requirements": ["pywmspro==0.3.3"] + "requirements": ["pywmspro==0.3.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9768b7302b603c..e582e5fba0b81f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2799,7 +2799,7 @@ pywilight==0.0.74 pywizlight==0.6.3 # homeassistant.components.wmspro -pywmspro==0.3.3 +pywmspro==0.3.4 # homeassistant.components.ws66i pyws66i==1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e22bcda28798ba..e557361ee3a0c7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2398,7 +2398,7 @@ pywilight==0.0.74 pywizlight==0.6.3 # homeassistant.components.wmspro -pywmspro==0.3.3 +pywmspro==0.3.4 # homeassistant.components.ws66i pyws66i==1.1 diff --git a/tests/components/nest/test_climate.py b/tests/components/nest/test_climate.py index 76a9a52f2de248..e45fc5752d7224 100644 --- a/tests/components/nest/test_climate.py +++ b/tests/components/nest/test_climate.py @@ -39,11 +39,16 @@ STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError, ServiceValidationError +from homeassistant.exceptions import ( + HomeAssistantError, + ServiceNotSupported, + ServiceValidationError, +) from .common import ( DEVICE_COMMAND, DEVICE_ID, + DOMAIN, CreateDevice, PlatformSetup, create_nest_event, @@ -1085,6 +1090,191 @@ async def test_thermostat_set_fan( } +async def test_set_fan_timer( + hass: HomeAssistant, + setup_platform: PlatformSetup, + auth: FakeAuth, + create_device: CreateDevice, +) -> None: + """Test the set_fan_timer service.""" + create_device.create( + { + "sdm.devices.traits.Fan": { + "timerMode": "OFF", + }, + "sdm.devices.traits.ThermostatHvac": {"status": "HEATING"}, + "sdm.devices.traits.ThermostatMode": { + "availableModes": ["HEAT", "OFF"], + "mode": "HEAT", + }, + "sdm.devices.traits.TemperatureTrait": {"ambientTemperatureCelsius": 20.0}, + } + ) + await setup_platform() + + # Call the set_fan_timer service + await hass.services.async_call( + DOMAIN, + "set_fan_timer", + { + "entity_id": "climate.my_thermostat", + "duration": {"minutes": 15}, + }, + blocking=True, + ) + + assert auth.method == "post" + assert auth.url == DEVICE_COMMAND + assert auth.json == { + "command": "sdm.devices.commands.Fan.SetTimer", + "params": { + "duration": "900s", + "timerMode": "ON", + }, + } + + +async def test_set_fan_timer_hvac_off( + hass: HomeAssistant, + setup_platform: PlatformSetup, + auth: FakeAuth, + create_device: CreateDevice, +) -> None: + """Test the set_fan_timer service when HVAC is off.""" + create_device.create( + { + "sdm.devices.traits.Fan": { + "timerMode": "OFF", + }, + "sdm.devices.traits.ThermostatHvac": {"status": "OFF"}, + "sdm.devices.traits.ThermostatMode": { + "availableModes": ["HEAT", "OFF"], + "mode": "OFF", + }, + } + ) + await setup_platform() + + with pytest.raises(HomeAssistantError, match="Cannot turn on fan"): + await hass.services.async_call( + DOMAIN, + "set_fan_timer", + { + "entity_id": "climate.my_thermostat", + "duration": {"minutes": 15}, + }, + blocking=True, + ) + + +async def test_set_fan_timer_no_fan( + hass: HomeAssistant, + setup_platform: PlatformSetup, + create_device: CreateDevice, +) -> None: + """Test the set_fan_timer service when device does not support fan.""" + create_device.create( + { + "sdm.devices.traits.ThermostatHvac": {"status": "HEATING"}, + "sdm.devices.traits.ThermostatMode": { + "availableModes": ["HEAT", "OFF"], + "mode": "HEAT", + }, + } + ) + await setup_platform() + + with pytest.raises(ServiceNotSupported, match="does not support action"): + await hass.services.async_call( + DOMAIN, + "set_fan_timer", + { + "entity_id": "climate.my_thermostat", + "duration": {"minutes": 15}, + }, + blocking=True, + ) + + +async def test_set_fan_timer_api_error( + hass: HomeAssistant, + setup_platform: PlatformSetup, + auth: FakeAuth, + create_device: CreateDevice, +) -> None: + """Test the set_fan_timer service with an API error.""" + create_device.create( + { + "sdm.devices.traits.Fan": { + "timerMode": "OFF", + }, + "sdm.devices.traits.ThermostatHvac": {"status": "HEATING"}, + "sdm.devices.traits.ThermostatMode": { + "availableModes": ["HEAT", "OFF"], + "mode": "HEAT", + }, + } + ) + await setup_platform() + + auth.responses = [aiohttp.web.Response(status=HTTPStatus.INTERNAL_SERVER_ERROR)] + with pytest.raises(HomeAssistantError, match="Error setting .* fan timer"): + await hass.services.async_call( + DOMAIN, + "set_fan_timer", + { + "entity_id": "climate.my_thermostat", + "duration": {"minutes": 15}, + }, + blocking=True, + ) + + +async def test_set_fan_timer_invalid_duration( + hass: HomeAssistant, + setup_platform: PlatformSetup, + create_device: CreateDevice, +) -> None: + """Test the set_fan_timer service with invalid durations.""" + create_device.create( + { + "sdm.devices.traits.Fan": { + "timerMode": "OFF", + }, + "sdm.devices.traits.ThermostatHvac": {"status": "HEATING"}, + "sdm.devices.traits.ThermostatMode": { + "availableModes": ["HEAT", "OFF"], + "mode": "HEAT", + }, + } + ) + await setup_platform() + + # Duration too long (over 12 hours) + with pytest.raises(ValueError, match="must be between 1 and 43200 seconds"): + await hass.services.async_call( + DOMAIN, + "set_fan_timer", + { + "entity_id": "climate.my_thermostat", + "duration": {"hours": 16}, + }, + blocking=True, + ) + + # Duration zero + with pytest.raises(ValueError, match="must be between 1 and 43200 seconds"): + await hass.services.async_call( + DOMAIN, + "set_fan_timer", + { + "entity_id": "climate.my_thermostat", + "duration": {"seconds": 0}, + }, + blocking=True, + ) + + async def test_thermostat_set_fan_when_off( hass: HomeAssistant, setup_platform: PlatformSetup, diff --git a/tests/components/solaredge/__init__.py b/tests/components/solaredge/__init__.py index c2a54cfafb623c..010251296101ff 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 664716be1cf67c..21cd8153d51c62 100644 --- a/tests/components/solaredge/conftest.py +++ b/tests/components/solaredge/conftest.py @@ -5,6 +5,11 @@ 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 00000000000000..a17e31a27a0d8d --- /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 00000000000000..6a66c810f3959d --- /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 00000000000000..971ec2e6ad5dab --- /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 00000000000000..8e3c2ba0591c20 --- /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 00000000000000..22c12c9d18a4c4 --- /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 00000000000000..21f4065467139b --- /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 00000000000000..679a530201f3f0 --- /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 982ccad2c92b08..c385e5d28d12be 100644 --- a/tests/components/solaredge/test_coordinator.py +++ b/tests/components/solaredge/test_coordinator.py @@ -4,6 +4,7 @@ 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 @@ 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.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 fca1bb7876b342..6901a46ba5208e 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.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 0dd514d0e80ba9..2e1527f6ba5ab7 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 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}, - ) +from . import setup_integration +from .conftest import SITE_ID - -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 @@ def mock_config_entry() -> MockConfigEntry: } -@patch("homeassistant.components.solaredge.SolarEdge") +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_all_entities( + recorder_mock: Recorder, + hass: HomeAssistant, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + mock_config_entry: MockConfigEntry, + solaredge_api: Mock, +) -> None: + """Test all 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( - 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 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 + assert float(hass.states.get(bat2_charge).state) == 700.0 + assert float(hass.states.get(bat2_soc).state) == 80.0 - 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, +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" + assert hass.states.get(charge_entry).state == STATE_UNAVAILABLE + assert hass.states.get(discharge_entry).state == STATE_UNAVAILABLE - state = hass.states.get(discharge_entry) - assert state is not None - assert 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 + assert hass.states.get(charge_entry).state == STATE_UNAVAILABLE + assert hass.states.get(discharge_entry).state == STATE_UNAVAILABLE - # 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, +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_storage_service_deferred_after_inventory_failure( recorder_mock: Recorder, hass: HomeAssistant, freezer: FrozenDateTimeFactory, - mock_solaredge_api: AsyncMock, mock_config_entry: MockConfigEntry, + solaredge_api: Mock, 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 + """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) - charge_entry = entity_registry.async_get_entity_id( - "sensor", DOMAIN, f"{SITE_ID}_storage_charge_energy" + # 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 ) - assert charge_entry is not None - state = hass.states.get(charge_entry) - assert state is not None - assert state.state == "unavailable" + # 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) -@patch("homeassistant.components.solaredge.SolarEdge") -async def test_storage_service_deferred_after_inventory_failure( - mock_solaredge: MagicMock, + 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, - freezer: FrozenDateTimeFactory, - mock_solaredge_api: AsyncMock, mock_config_entry: MockConfigEntry, + solaredge_api: Mock, entity_registry: er.EntityRegistry, + storage_response: dict, + expected_charge_state: str, ) -> 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 data service tolerates batteries without serial / telemetries / single telemetry.""" + solaredge_api.get_storage_data.return_value = storage_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) - # 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 + assert charge_entry is not None + state = hass.states.get(charge_entry) + assert state is not None + assert state.state == expected_charge_state - # 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) +@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) - # Storage sensors should now exist + # 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" ) - 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 + # 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 == [] -@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_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 - - 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": []}} - ) + """Test storage service stays idle when inventory recovers but reports no batteries.""" + solaredge_api.get_inventory.side_effect = KeyError("Inventory") + + await setup_integration(hass, mock_config_entry) + + # Inventory recovers but reports zero batteries. + solaredge_api.get_inventory.side_effect = None + solaredge_api.get_inventory.return_value = {"Inventory": {"batteries": []}} freezer.tick(INVENTORY_UPDATE_DELAY) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) - # Storage sensors should still not exist charge_entry = entity_registry.async_get_entity_id( "sensor", DOMAIN, f"{SITE_ID}_storage_charge_energy" ) diff --git a/tests/components/weatherflow_cloud/test_sensor.py b/tests/components/weatherflow_cloud/test_sensor.py index 0d4f63cf03e748..62c1779b667562 100644 --- a/tests/components/weatherflow_cloud/test_sensor.py +++ b/tests/components/weatherflow_cloud/test_sensor.py @@ -1,5 +1,6 @@ """Tests for the WeatherFlow Cloud sensor platform.""" +from dataclasses import replace from datetime import timedelta from unittest.mock import AsyncMock, MagicMock, patch @@ -17,7 +18,7 @@ WeatherFlowWebsocketSensorObservation, WeatherFlowWebsocketSensorWind, ) -from homeassistant.const import STATE_UNKNOWN, Platform +from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -90,6 +91,34 @@ async def test_all_entities_with_lightning_error( ) +async def test_rest_sensor_unavailable_with_empty_observation( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_rest_api: AsyncMock, + mock_websocket_api: AsyncMock, + freezer: FrozenDateTimeFactory, +) -> None: + """Test REST sensors are unavailable when no current observation is returned.""" + with patch( + "homeassistant.components.weatherflow_cloud.PLATFORMS", [Platform.SENSOR] + ): + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("sensor.my_home_station_temperature").state == "10.5" + + all_data = await mock_rest_api.get_all_data() + all_data[24432].observation = replace(all_data[24432].observation, obs=[]) + mock_rest_api.get_all_data.return_value = all_data + + freezer.tick(timedelta(minutes=5)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert ( + hass.states.get("sensor.my_home_station_temperature").state == STATE_UNAVAILABLE + ) + + async def test_websocket_sensor_observation( hass: HomeAssistant, mock_config_entry: MockConfigEntry,