Project skeleton
Some checks failed
HACS Validation / validate (push) Failing after 38s
Validate with hassfest / validate (push) Failing after 41s

This commit is contained in:
Adrian Rumpold
2025-04-07 10:17:40 +02:00
commit 5180992e98
23 changed files with 3302 additions and 0 deletions

1
tests/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""Tests package for PV Microinverter integration."""

56
tests/conftest.py Normal file
View File

@@ -0,0 +1,56 @@
"""Pytest fixtures for PV Microinverter tests."""
from datetime import datetime
from unittest.mock import AsyncMock, MagicMock
import pytest
from pv_microinverter.api import PVMicroinverterApiClient
from pv_microinverter.const import (
CONF_STATION_ID,
CONF_UPDATE_INTERVAL,
DEFAULT_UPDATE_INTERVAL,
PVMicroinverterData,
)
from pv_microinverter.coordinator import (
PVMicroinverterDataUpdateCoordinator,
)
@pytest.fixture
def mock_api_client():
"""Return a mocked PV Microinverter API client."""
client = MagicMock(spec=PVMicroinverterApiClient)
client.async_get_data = AsyncMock(
return_value=PVMicroinverterData(
current_power=500.0,
today_energy=2.5,
lifetime_energy=150.0,
last_updated=datetime.now().isoformat(),
)
)
client.async_check_connection = AsyncMock(return_value=True)
return client
@pytest.fixture
def mock_config_entry():
"""Return a mock config entry."""
return MagicMock(
data={
CONF_STATION_ID: "test_station_id",
CONF_UPDATE_INTERVAL: DEFAULT_UPDATE_INTERVAL,
},
entry_id="test_entry_id",
)
@pytest.fixture
def mock_coordinator(mock_api_client):
"""Return a mock coordinator."""
coordinator = MagicMock(spec=PVMicroinverterDataUpdateCoordinator)
coordinator.api_client = mock_api_client
coordinator.data = mock_api_client.async_get_data.return_value
coordinator.last_update_success = True
coordinator.async_config_entry_first_refresh = AsyncMock()
return coordinator

206
tests/test_api.py Normal file
View File

@@ -0,0 +1,206 @@
"""Tests for the PV Microinverter API client."""
import json
from unittest.mock import AsyncMock, MagicMock
import aiohttp
import pytest
from aiohttp import ClientResponseError, ClientSession
from custom_components.pv_microinverter.api import (
PVMicroinverterApiClient,
PVMicroinverterApiClientError,
)
@pytest.fixture
def mock_session():
"""Return a mocked aiohttp client session."""
session = MagicMock(spec=ClientSession)
session.get = AsyncMock()
return session
@pytest.fixture
def api_client(mock_session):
"""Return a new API client with a mocked session."""
return PVMicroinverterApiClient(
session=mock_session,
station_id="test_station_id",
base_url="https://api.example.com/v1",
)
@pytest.fixture
def mock_response():
"""Return a mocked API response."""
mock = MagicMock()
mock.raise_for_status = MagicMock()
mock.json = AsyncMock(
return_value={
"current_power": 500.5,
"today_energy": 3.75,
"lifetime_energy": 1250.25,
"last_updated": "2023-04-01T12:00:00Z",
}
)
return mock
@pytest.mark.asyncio
async def test_async_get_data_success(api_client, mock_session, mock_response):
"""Test successful data retrieval."""
# Setup the mock response
mock_session.get.return_value = mock_response
# Call the method
data = await api_client.async_get_data()
# Verify the API call
mock_session.get.assert_called_once_with(
"https://api.example.com/v1/systems/test_system_id/stats",
headers={
"Authorization": "Bearer test_api_key",
"Content-Type": "application/json",
},
)
# Verify the response processing
assert data.current_power == 500.5
assert data.today_energy == 3.75
assert data.lifetime_energy == 1250.25
assert data.last_updated == "2023-04-01T12:00:00Z"
@pytest.mark.asyncio
async def test_async_get_data_http_error(api_client, mock_session):
"""Test handling of HTTP errors."""
# Setup the mock to raise an error
error_response = MagicMock()
error_response.raise_for_status.side_effect = ClientResponseError(
request_info=MagicMock(),
history=None,
status=401,
message="Unauthorized",
headers=None,
)
mock_session.get.return_value = error_response
# Call the method and expect an exception
with pytest.raises(PVMicroinverterApiClientError) as excinfo:
await api_client.async_get_data()
# Verify the error message
assert "Error fetching data from API" in str(excinfo.value)
@pytest.mark.asyncio
async def test_async_get_data_connection_error(api_client, mock_session):
"""Test handling of connection errors."""
# Setup the mock to raise a connection error
mock_session.get.side_effect = aiohttp.ClientConnectionError("Connection refused")
# Call the method and expect an exception
with pytest.raises(PVMicroinverterApiClientError) as excinfo:
await api_client.async_get_data()
# Verify the error message
assert "Error fetching data from API" in str(excinfo.value)
@pytest.mark.asyncio
async def test_async_get_data_invalid_json(api_client, mock_session, mock_response):
"""Test handling of invalid JSON responses."""
# Setup the mock to return invalid JSON
mock_response.json.side_effect = json.JSONDecodeError("Invalid JSON", "", 0)
mock_session.get.return_value = mock_response
# Call the method and expect an exception
with pytest.raises(PVMicroinverterApiClientError) as excinfo:
await api_client.async_get_data()
# Verify the error message
assert "Unexpected error occurred" in str(excinfo.value)
@pytest.mark.asyncio
async def test_async_get_data_missing_fields(api_client, mock_session, mock_response):
"""Test handling of responses with missing fields."""
# Setup the mock to return a response with missing fields
mock_response.json.return_value = {"some_other_field": "value"}
mock_session.get.return_value = mock_response
# Call the method - it should handle missing fields gracefully
data = await api_client.async_get_data()
# Verify default values are used
assert data.current_power == 0
assert data.today_energy == 0
assert data.lifetime_energy == 0
assert data.last_updated is not None # Should default to current time
@pytest.mark.asyncio
async def test_async_check_connection_success(api_client, mock_session):
"""Test successful connection check."""
# Setup the mock response
mock_response = MagicMock()
mock_response.raise_for_status = MagicMock()
mock_session.get.return_value = mock_response
# Call the method
result = await api_client.async_check_connection()
# Verify the API call
mock_session.post.assert_called_once_with(
f"{api_client._base_url}/GetStationInfo",
headers={
"Authorization": "Bearer test_api_key",
"Content-Type": "application/json",
},
)
# Verify the result
assert result is True
@pytest.mark.asyncio
async def test_async_check_connection_failure(api_client, mock_session):
"""Test failed connection check."""
# Setup the mock to raise an error
mock_session.get.side_effect = aiohttp.ClientError("Connection error")
# Call the method
result = await api_client.async_check_connection()
# Verify the result
assert result is False
@pytest.mark.asyncio
async def test_process_data_with_various_types(api_client):
"""Test data processing with various data types."""
# Test with string values that should be converted to float
data_with_strings = {
"current_power": "450.75",
"today_energy": "2.5",
"lifetime_energy": "1000",
"last_updated": "2023-04-01T14:30:00Z",
}
result = api_client._process_data(data_with_strings)
assert result.current_power == 450.75
assert result.today_energy == 2.5
assert result.lifetime_energy == 1000.0
assert result.last_updated == "2023-04-01T14:30:00Z"
# Test with mixed types
data_mixed = {
"current_power": 300,
"today_energy": 1.5,
"lifetime_energy": "750.5",
"last_updated": "2023-04-01T14:30:00Z",
}
result = api_client._process_data(data_mixed)
assert result.current_power == 300.0
assert result.today_energy == 1.5
assert result.lifetime_energy == 750.5
assert result.last_updated == "2023-04-01T14:30:00Z"

72
tests/test_sensor.py Normal file
View File

@@ -0,0 +1,72 @@
"""Tests for the PV Microinverter sensor platform."""
from datetime import datetime
from unittest.mock import MagicMock
import pytest
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
from homeassistant.const import UnitOfEnergy, UnitOfPower
from pv_microinverter.const import PVMicroinverterData
from pv_microinverter.coordinator import (
PVMicroinverterDataUpdateCoordinator,
)
from pv_microinverter.sensor import PVMicroinverterSensor
@pytest.mark.asyncio
async def test_sensor_initialization():
"""Test sensor initialization."""
# Mock data and coordinator
mock_data = PVMicroinverterData(
current_power=500.0,
today_energy=2.5,
lifetime_energy=150.0,
last_updated=datetime.now().isoformat(),
)
mock_coordinator = MagicMock(spec=PVMicroinverterDataUpdateCoordinator)
mock_coordinator.data = mock_data
mock_coordinator.last_update_success = True
# Test current power sensor
current_power_sensor = PVMicroinverterSensor(
coordinator=mock_coordinator,
system_id="test_system",
sensor_type="current_power",
sensor_info={
"name": "Current Power",
"icon": "mdi:solar-power",
"unit": "W",
"device_class": "power",
"state_class": "measurement",
},
)
# Verify sensor properties
assert current_power_sensor.name == "Current Power"
assert current_power_sensor.native_unit_of_measurement == UnitOfPower.WATT
assert current_power_sensor.device_class == SensorDeviceClass.POWER
assert current_power_sensor.state_class == SensorStateClass.MEASUREMENT
assert current_power_sensor.native_value == 500.0
# Test today's energy sensor
today_energy_sensor = PVMicroinverterSensor(
coordinator=mock_coordinator,
system_id="test_system",
sensor_type="today_energy",
sensor_info={
"name": "Today's Energy",
"icon": "mdi:solar-power",
"unit": "kWh",
"device_class": "energy",
"state_class": "total_increasing",
},
)
# Verify sensor properties
assert today_energy_sensor.name == "Today's Energy"
assert today_energy_sensor.native_unit_of_measurement == UnitOfEnergy.KILO_WATT_HOUR
assert today_energy_sensor.device_class == SensorDeviceClass.ENERGY
assert today_energy_sensor.state_class == SensorStateClass.TOTAL_INCREASING
assert today_energy_sensor.native_value == 2.5

42
tests/test_units.py Normal file
View File

@@ -0,0 +1,42 @@
import pytest
from pv_microinverter.units import Dimension, SIUnit
def test_siunit_parse_base_unit():
unit = SIUnit.parse("W")
# Check that parsing base unit returns the registered unit
assert unit.name == "Watt"
assert unit.symbol == "W"
assert unit.factor == 1
def test_siunit_parse_prefixed_unit():
unit = SIUnit.parse("kW")
# The expected unit is created by prefix "k" and the base unit "Watt"
assert unit.name == "kWatt"
assert unit.symbol == "kW"
assert unit.factor == 1000 # 1e3 multiplier
def test_dimension_parse_valid():
# Parsing a valid dimension string should succeed
dim = Dimension.parse("3.5 kW")
assert dim.value == "3.5"
# Check unit attributes from SIUnit.parse
assert dim.unit.name == "kWatt"
assert dim.unit.symbol == "kW"
# Check conversion to base unit: 3.5 * 1000 = 3500.0
assert dim.to_base_unit() == 3500.0
def test_dimension_parse_invalid_format():
# Missing space between value and unit should raise a ValueError
with pytest.raises(ValueError):
Dimension.parse("3.5kW")
def test_siunit_parse_unknown_unit():
# Attempting to parse an unknown unit should raise a ValueError
with pytest.raises(ValueError):
SIUnit.parse("invalid")