Add SolarEdge Optimizers Home Assistant integration
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""Tests for SolarEdge Optimizers."""
|
||||
@@ -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."""
|
||||
@@ -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",
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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"
|
||||
@@ -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",
|
||||
}
|
||||
Reference in New Issue
Block a user