Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions homeassistant/components/solax/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
"""The solax component."""

import asyncio
from dataclasses import dataclass
from datetime import timedelta
from importlib.metadata import entry_points
import logging

from solax import InverterResponse, RealTimeAPI, real_time_api
from solax import InverterResponse, RealTimeAPI, discover
from solax.inverter import InverterError

from homeassistant.config_entries import ConfigEntry
Expand All @@ -13,12 +15,17 @@
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.update_coordinator import UpdateFailed

from .const import CONF_SOLAX_INVERTER, SOLAX_ENTRY_POINT_GROUP
from .coordinator import SolaxDataUpdateCoordinator

PLATFORMS = [Platform.SENSOR]

SCAN_INTERVAL = timedelta(seconds=30)

INVERTERS_ENTRY_POINTS = {
ep.name: ep.load() for ep in entry_points(group=SOLAX_ENTRY_POINT_GROUP)
}


@dataclass(slots=True)
class SolaxData:
Expand All @@ -36,12 +43,20 @@ class SolaxData:
async def async_setup_entry(hass: HomeAssistant, entry: SolaxConfigEntry) -> bool:
"""Set up the sensors from a ConfigEntry."""

if inverter_name := entry.data.get(CONF_SOLAX_INVERTER):
invset = {INVERTERS_ENTRY_POINTS[inverter_name]}
else:
invset = set(INVERTERS_ENTRY_POINTS.values())

try:
api = await real_time_api(
inverter = await discover(
entry.data[CONF_IP_ADDRESS],
entry.data[CONF_PORT],
entry.data[CONF_PASSWORD],
inverters=invset,
return_when=asyncio.FIRST_COMPLETED,
)
api = RealTimeAPI(inverter)
except Exception as err:
raise ConfigEntryNotReady from err

Expand Down
34 changes: 28 additions & 6 deletions homeassistant/components/solax/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,39 +1,61 @@
"""Config flow for solax integration."""

import asyncio
from importlib.metadata import entry_points
import logging
from typing import Any, override

from solax import real_time_api
from solax import discover
from solax.discovery import DiscoveryError
import voluptuous as vol

from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import config_validation as cv, selector

from .const import DOMAIN
from .const import CONF_SOLAX_INVERTER, DOMAIN, SOLAX_ENTRY_POINT_GROUP

_LOGGER = logging.getLogger(__name__)

DEFAULT_PORT = 80
DEFAULT_PASSWORD = ""

INVERTERS_ENTRY_POINTS = {
ep.name: ep.load() for ep in entry_points(group=SOLAX_ENTRY_POINT_GROUP)
}

STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_IP_ADDRESS): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_PASSWORD, default=DEFAULT_PASSWORD): cv.string,
vol.Optional(CONF_SOLAX_INVERTER): selector.SelectSelector(
selector.SelectSelectorConfig(
options=list(INVERTERS_ENTRY_POINTS.keys()),
mode=selector.SelectSelectorMode.DROPDOWN,
multiple=False,
)
),
Comment thread
brew-your-own marked this conversation as resolved.
}
)


async def validate_api(data) -> str:
"""Validate the credentials."""

api = await real_time_api(
data[CONF_IP_ADDRESS], data[CONF_PORT], data[CONF_PASSWORD]
if inverter_name := data.get(CONF_SOLAX_INVERTER):
invset = {INVERTERS_ENTRY_POINTS[inverter_name]}
else:
invset = set(INVERTERS_ENTRY_POINTS.values())
Comment thread
brew-your-own marked this conversation as resolved.

inverter = await discover(
data[CONF_IP_ADDRESS],
data[CONF_PORT],
data[CONF_PASSWORD],
inverters=invset,
return_when=asyncio.FIRST_COMPLETED,
)
response = await api.get_data()
response = await inverter.get_data()
return response.serial_number


Expand Down
4 changes: 4 additions & 0 deletions homeassistant/components/solax/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@
DOMAIN = "solax"

MANUFACTURER = "SolaX Power"

CONF_SOLAX_INVERTER = "solax.inverters"

SOLAX_ENTRY_POINT_GROUP = "solax.inverter"
4 changes: 4 additions & 0 deletions homeassistant/components/solax/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ async def async_setup_entry(
resp = coordinator.data
serial = resp.serial_number
version = resp.version
model = type(api.inverter).__name__
entities: list[InverterSensorEntity] = []
for sensor, (idx, measurement) in api.inverter.sensor_map().items():
description = SENSOR_DESCRIPTIONS[(measurement.unit, measurement.is_monotonic)]
Expand All @@ -109,6 +110,7 @@ async def async_setup_entry(
uid,
serial,
version,
model,
sensor,
description,
)
Expand All @@ -128,6 +130,7 @@ def __init__(
uid: str,
serial: str,
version: str,
model: str,
key: str,
entity_description: SensorEntityDescription,
) -> None:
Expand All @@ -139,6 +142,7 @@ def __init__(
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, serial)},
manufacturer=MANUFACTURER,
model=f"Inverter: {model}",
name=f"{manufacturer} {serial}",
sw_version=version,
)
Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/solax/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"data": {
"ip_address": "[%key:common::config_flow::data::ip%]",
"password": "[%key:common::config_flow::data::password%]",
"port": "[%key:common::config_flow::data::port%]"
"port": "[%key:common::config_flow::data::port%]",
"solax.inverters": "Inverter model"
}
}
}
Expand Down
69 changes: 49 additions & 20 deletions tests/components/solax/test_config_flow.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
"""Tests for the solax config flow."""

from unittest.mock import patch
import asyncio
from unittest.mock import AsyncMock, MagicMock, patch

from solax import RealTimeAPI
from solax.inverter import InverterResponse
import pytest
from solax.inverter import Inverter, InverterResponse
from solax.inverters import X1MiniV34

from homeassistant import config_entries
from homeassistant.components.solax.const import DOMAIN
from homeassistant.components.solax.config_flow import INVERTERS_ENTRY_POINTS
from homeassistant.components.solax.const import CONF_SOLAX_INVERTER, DOMAIN
from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType


def __mock_real_time_api_success():
return RealTimeAPI(X1MiniV34)
def __mock_discover_success() -> MagicMock:
return MagicMock(spec=X1MiniV34, get_data=AsyncMock(return_value=__mock_get_data()))


def __mock_get_data():
Expand All @@ -27,38 +29,65 @@ def __mock_get_data():
)


async def test_form_success(hass: HomeAssistant) -> None:
@pytest.mark.parametrize(
("user_input_extra", "expected_inverters"),
[
pytest.param(
{CONF_SOLAX_INVERTER: "x1_mini_v34"},
{X1MiniV34},
id="explicit_inverter",
),
pytest.param(
{},
set(INVERTERS_ENTRY_POINTS.values()),
id="auto_discovery",
),
],
)
async def test_form_success(
hass: HomeAssistant,
user_input_extra: dict[str, str],
expected_inverters: set[type[Inverter]],
) -> None:
"""Test successful form."""
flow = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert flow["type"] is FlowResultType.FORM
assert flow["errors"] == {}

user_input = {
CONF_IP_ADDRESS: "192.168.1.87",
CONF_PORT: 80,
CONF_PASSWORD: "password",
**user_input_extra,
}

with (
patch(
"homeassistant.components.solax.config_flow.real_time_api",
return_value=__mock_real_time_api_success(),
),
patch("solax.RealTimeAPI.get_data", return_value=__mock_get_data()),
"homeassistant.components.solax.config_flow.discover",
return_value=__mock_discover_success(),
) as mock_discover,
patch(
"homeassistant.components.solax.async_setup_entry",
return_value=True,
) as mock_setup_entry,
):
entry_result = await hass.config_entries.flow.async_configure(
flow["flow_id"],
{CONF_IP_ADDRESS: "192.168.1.87", CONF_PORT: 80, CONF_PASSWORD: "password"},
flow["flow_id"], user_input
)
await hass.async_block_till_done()

mock_discover.assert_called_once_with(
"192.168.1.87",
80,
"password",
inverters=expected_inverters,
return_when=asyncio.FIRST_COMPLETED,
)
assert entry_result["type"] is FlowResultType.CREATE_ENTRY
assert entry_result["title"] == "ABCDEFGHIJ"
assert entry_result["data"] == {
CONF_IP_ADDRESS: "192.168.1.87",
CONF_PORT: 80,
CONF_PASSWORD: "password",
}
assert entry_result["data"] == user_input
assert len(mock_setup_entry.mock_calls) == 1


Expand All @@ -71,7 +100,7 @@ async def test_form_connect_error(hass: HomeAssistant) -> None:
assert flow["errors"] == {}

with patch(
"homeassistant.components.solax.config_flow.real_time_api",
"homeassistant.components.solax.config_flow.discover",
side_effect=ConnectionError,
):
entry_result = await hass.config_entries.flow.async_configure(
Expand All @@ -92,7 +121,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None:
assert flow["errors"] == {}

with patch(
"homeassistant.components.solax.config_flow.real_time_api",
"homeassistant.components.solax.config_flow.discover",
side_effect=Exception,
):
entry_result = await hass.config_entries.flow.async_configure(
Expand Down
Loading