Skip to content
Merged
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
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,34 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.7.0] - 2026-10-07

### Added

- Renewable Component Library (RCL) integration via `solarfarmer.rcl`:
`list_modules()`, `list_inverters()`, `download_file()`, and
`get_rate_limit_status()`.
- RCL catalog typing and generated wire models, including public filter/field
types and response item `TypedDict`s.
- `RCLRateLimitInfo` helper type with reset time and usage metrics.
- `PVSystem` integration methods:
`set_module_from_rcl()` and `set_inverter_from_rcl()` for catalog search,
download, and assignment of PAN/OND files.
- New RCL documentation and examples:
`docs/getting-started/rcl-component-library.md` and
`docs/notebooks/Example_RCL_Catalog.ipynb`.

### Changed

- SDK documentation and navigation updated to include RCL workflows and API
reference.

### Fixed

- Flush-mount `PVSystem` tilt handling now preserves `plant.tilt` in mounting
specifications instead of forcing `0.0`, aligning flush-mount behaviour with
user-provided fixed-tilt inputs.

## [0.6.0] - 2026-09-24

### Added
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "dnv-solarfarmer"
version = "0.6.0"
version = "0.7.0"
description = "Python SDK for SolarFarmer, a bankable solar PV design and energy yield assessment tool by DNV."
readme = "README.md"
requires-python = ">=3.10"
Expand Down
39 changes: 27 additions & 12 deletions tests/test_construct_plant.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
from pathlib import Path

import pytest

Expand Down Expand Up @@ -152,52 +153,66 @@ def test_flush_mount_uses_plant_tilt(self):
class TestSpecIdDerivation:
"""Verify spec IDs are derived via Path.stem (last-dot split), not split('.')[0]."""

@staticmethod
def _create_multi_dot_alias(source: "Path", target: "Path") -> None:
"""Create a temp alias file with a multi-dot name for stem extraction tests."""
try:
target.symlink_to(source)
return
except OSError:
pass

try:
target.hardlink_to(source)
return
except OSError:
pass

# Last-resort fallback for platforms without link privileges.
target.write_bytes(source.read_bytes())

def test_multi_dot_pan_filename_produces_correct_spec_id(self, bern_2d_racks_inputs):
"""PAN filenames with multiple dots must use Path.stem for spec ID."""
from pathlib import Path

from solarfarmer.models.pvsystem.pvsystem import get_module_info_from_pan

p = PVSystem(latitude=46.95, longitude=7.44)
original = Path(bern_2d_racks_inputs) / "CanadianSolar_CS6U-330M_APP.PAN"
multi_dot = Path(bern_2d_racks_inputs) / "Trina_TSM-DEG19C.20-550_APP.PAN"

created_link = False
created_temp_file = False
try:
if not multi_dot.exists():
multi_dot.symlink_to(original)
created_link = True
self._create_multi_dot_alias(original, multi_dot)
created_temp_file = True
p.pan_files = {"TestModule": str(multi_dot)}
info = get_module_info_from_pan(p)
# Path.stem gives "Trina_TSM-DEG19C.20-550_APP"
# split(".")[0] would give "Trina_TSM-DEG19C" — wrong
assert info["pan_filename"] == "Trina_TSM-DEG19C.20-550_APP"
finally:
if created_link and multi_dot.exists():
if created_temp_file and multi_dot.exists():
multi_dot.unlink()

def test_multi_dot_ond_filename_produces_correct_spec_id(self, bern_2d_racks_inputs):
"""OND filenames with multiple dots must use Path.stem for spec ID."""
from pathlib import Path

from solarfarmer.models.pvsystem.pvsystem import get_inverter_info_from_ond

p = PVSystem(latitude=46.95, longitude=7.44)
original = Path(bern_2d_racks_inputs) / "Sungrow_SG125HV_APP.OND"
multi_dot = Path(bern_2d_racks_inputs) / "SMA_Sunny-Central.2200-UP.OND"

created_link = False
created_temp_file = False
try:
if not multi_dot.exists():
multi_dot.symlink_to(original)
created_link = True
self._create_multi_dot_alias(original, multi_dot)
created_temp_file = True
p.ond_files = {"TestInverter": str(multi_dot)}
info = get_inverter_info_from_ond(p)
# Path.stem gives "SMA_Sunny-Central.2200-UP"
# split(".")[0] would give "SMA_Sunny-Central" — wrong
assert info["ond_filename"] == "SMA_Sunny-Central.2200-UP"
finally:
if created_link and multi_dot.exists():
if created_temp_file and multi_dot.exists():
multi_dot.unlink()


Expand Down
8 changes: 4 additions & 4 deletions tests/test_endpoint_about.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def test_about_version_structure(self, api_key):
assert len(result["solarFarmerCoreVersion"]) > 0
assert len(result["solarFarmerApiVersion"]) > 0

@pytest.mark.parametrize("version,expected_version_prefix", [("v5", "5."), ("v6", "6.")])
@pytest.mark.parametrize("version,expected_version_prefix", [("v6", "6."), ("v7", "7.")])
def test_about_all_versions_return_proper_structure(
self, api_key, version, expected_version_prefix
):
Expand Down Expand Up @@ -88,14 +88,14 @@ def test_failure_returns_empty_dict(self, mock_client_cls):
@patch("solarfarmer.endpoint_about.Client")
def test_version_forwarded_to_build_api_url(self, mock_client_cls, mock_build_url):
"""The version argument is forwarded to build_api_url."""
mock_build_url.return_value = "http://mock/v5"
mock_build_url.return_value = "http://mock/v7"
mock_client = MagicMock()
mock_client_cls.return_value = mock_client
mock_client.get.return_value = self._ok_response()

solarfarmer.about(version="v5", api_key="test_key")
solarfarmer.about(version="v7", api_key="test_key")

mock_build_url.assert_called_once_with("v5")
mock_build_url.assert_called_once_with("v7")

@patch("solarfarmer.endpoint_about.Client")
def test_malformed_success_response_logs_error_and_returns_data(self, mock_client_cls, caplog):
Expand Down
Loading