Add SolarEdge Optimizers Home Assistant integration

This commit is contained in:
2026-08-10 09:31:43 +02:00
commit 2085316d1f
23 changed files with 3956 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
"""Tests for SolarEdge Optimizers."""
+12
View File
@@ -0,0 +1,12 @@
"""Test fixtures for SolarEdge Optimizers."""
from __future__ import annotations
import pytest
pytest_plugins = "pytest_homeassistant_custom_component"
@pytest.fixture(autouse=True)
def auto_enable_custom_integrations(enable_custom_integrations: None) -> None:
"""Enable loading custom integrations in all tests."""
+35
View File
@@ -0,0 +1,35 @@
"""Sample App API data."""
SAMPLE_PAYLOAD = {
"status": "ok",
"siteId": "4886699",
"date": "2026-08-10",
"timeZone": "Europe/Berlin",
"fetchedAt": "2026-08-10T10:00:00.000Z",
"nextRefreshAt": "2026-08-10T10:10:00.000Z",
"optimizerCount": 2,
"successfulOptimizerCount": 2,
"failedOptimizerCount": 0,
"totalDailyEnergyWh": 2468.5,
"totalCurrentPowerW": 321.4,
"optimizers": [
{
"serial": "14F28854-E2",
"inverterId": "1",
"stringId": "1.1",
"optimizerId": "1.1.1",
"dailyEnergyWh": 1234.5,
"currentPowerW": 160.7,
"lastMeasurement": "2026-08-10T09:59:30.000Z",
},
{
"serial": "14F28855-E2",
"inverterId": "1",
"stringId": "1.1",
"optimizerId": "1.1.2",
"dailyEnergyWh": 1234.0,
"currentPowerW": 160.7,
"lastMeasurement": "2026-08-10T09:59:30.000Z",
},
],
}
+56
View File
@@ -0,0 +1,56 @@
"""Tests for the App API parser."""
from __future__ import annotations
from copy import deepcopy
import pytest
from custom_components.solaredge_optimizers.api import (
SolarEdgeOptimizerInvalidResponseError,
normalize_base_url,
parse_snapshot,
)
from .sample_data import SAMPLE_PAYLOAD
def test_parse_snapshot() -> None:
"""Parse a valid snapshot."""
snapshot = parse_snapshot(SAMPLE_PAYLOAD)
assert snapshot.site_id == "4886699"
assert snapshot.optimizer_count == 2
assert snapshot.optimizers[0].optimizer_id == "1.1.1"
assert snapshot.optimizers[0].daily_energy_wh == 1234.5
@pytest.mark.parametrize(
("value", "expected"),
[
("http://app:8099/", "http://app:8099"),
("http://app:8099/api/optimizers", "http://app:8099"),
("https://example.test/prefix/", "https://example.test/prefix"),
],
)
def test_normalize_base_url(value: str, expected: str) -> None:
"""Normalize supported URLs."""
assert normalize_base_url(value) == expected
@pytest.mark.parametrize(
"value", ["app:8099", "ftp://app/data", "http://user:password@app:8099"]
)
def test_reject_invalid_url(value: str) -> None:
"""Reject invalid or credential-bearing URLs."""
with pytest.raises(ValueError):
normalize_base_url(value)
def test_reject_duplicate_serial() -> None:
"""Reject ambiguous optimizer records."""
payload = deepcopy(SAMPLE_PAYLOAD)
payload["optimizers"][1]["serial"] = payload["optimizers"][0]["serial"]
with pytest.raises(SolarEdgeOptimizerInvalidResponseError):
parse_snapshot(payload)
+70
View File
@@ -0,0 +1,70 @@
"""Tests for the SolarEdge Optimizers config flow."""
from __future__ import annotations
from unittest.mock import AsyncMock, patch
from homeassistant import config_entries
from homeassistant.const import CONF_URL
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers.service_info.hassio import HassioServiceInfo
from custom_components.solaredge_optimizers.api import parse_snapshot
from custom_components.solaredge_optimizers.const import (
CONF_SCAN_INTERVAL,
DOMAIN,
)
from .sample_data import SAMPLE_PAYLOAD
async def test_user_flow(hass: HomeAssistant) -> None:
"""Configure the integration manually."""
with patch(
"custom_components.solaredge_optimizers.config_flow.validate_input",
AsyncMock(return_value=("http://app:8099", parse_snapshot(SAMPLE_PAYLOAD))),
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_USER},
data={CONF_URL: "http://app:8099", CONF_SCAN_INTERVAL: 600},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "SolarEdge Site 4886699"
assert result["data"][CONF_URL] == "http://app:8099"
async def test_hassio_discovery_flow(hass: HomeAssistant) -> None:
"""Configure the integration from App discovery."""
discovery = HassioServiceInfo(
config={"host": "abc-solaredge-optimizer-data", "port": 8099},
name="SolarEdge Optimizer Data",
slug="abc_solaredge_optimizer_data",
uuid="discovery-uuid",
)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_HASSIO},
data=discovery,
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "hassio_confirm"
with patch(
"custom_components.solaredge_optimizers.config_flow.validate_input",
AsyncMock(
return_value=(
"http://abc-solaredge-optimizer-data:8099",
parse_snapshot(SAMPLE_PAYLOAD),
)
),
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={}
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["data"][CONF_URL] == "http://abc-solaredge-optimizer-data:8099"
+56
View File
@@ -0,0 +1,56 @@
"""Tests for SolarEdge optimizer sensors."""
from __future__ import annotations
from unittest.mock import AsyncMock, patch
from homeassistant.const import CONF_URL
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers import entity_registry as er
from pytest_homeassistant_custom_component.common import MockConfigEntry
from custom_components.solaredge_optimizers.api import parse_snapshot
from custom_components.solaredge_optimizers.const import CONF_SCAN_INTERVAL, DOMAIN
from .sample_data import SAMPLE_PAYLOAD
async def test_optimizer_sensors(hass: HomeAssistant) -> None:
"""Create energy and power sensors for every optimizer."""
entry = MockConfigEntry(
domain=DOMAIN,
title="SolarEdge Site 4886699",
unique_id="4886699",
data={CONF_URL: "http://app:8099", CONF_SCAN_INTERVAL: 600},
)
entry.add_to_hass(hass)
with patch(
"custom_components.solaredge_optimizers.api."
"SolarEdgeOptimizerApiClient.async_get_optimizers",
AsyncMock(return_value=parse_snapshot(SAMPLE_PAYLOAD)),
):
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
entity_registry = er.async_get(hass)
entities = er.async_entries_for_config_entry(entity_registry, entry.entry_id)
assert len(entities) == 4
energy_entity = next(
entity for entity in entities if entity.unique_id.endswith("_daily_energy")
)
power_entity = next(
entity for entity in entities if entity.unique_id.endswith("_current_power")
)
assert hass.states.get(energy_entity.entity_id).state == "1234.5"
assert hass.states.get(power_entity.entity_id).state == "160.7"
device_registry = dr.async_get(hass)
devices = dr.async_entries_for_config_entry(device_registry, entry.entry_id)
assert len(devices) == 2
assert {device.serial_number for device in devices} == {
"14F28854-E2",
"14F28855-E2",
}