aiosolaredge-one 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aiosolaredge_one-0.2.0/.github/workflows/ci.yml +27 -0
- aiosolaredge_one-0.2.0/.github/workflows/publish.yml +61 -0
- aiosolaredge_one-0.2.0/.gitignore +21 -0
- aiosolaredge_one-0.2.0/CHANGELOG.md +22 -0
- aiosolaredge_one-0.2.0/LICENSE +21 -0
- aiosolaredge_one-0.2.0/PKG-INFO +102 -0
- aiosolaredge_one-0.2.0/README.md +51 -0
- aiosolaredge_one-0.2.0/RELEASING.md +83 -0
- aiosolaredge_one-0.2.0/pyproject.toml +60 -0
- aiosolaredge_one-0.2.0/src/aiosolaredge_one/__init__.py +66 -0
- aiosolaredge_one-0.2.0/src/aiosolaredge_one/budget.py +145 -0
- aiosolaredge_one-0.2.0/src/aiosolaredge_one/client.py +203 -0
- aiosolaredge_one-0.2.0/src/aiosolaredge_one/const.py +20 -0
- aiosolaredge_one-0.2.0/src/aiosolaredge_one/exceptions.py +38 -0
- aiosolaredge_one-0.2.0/src/aiosolaredge_one/models.py +235 -0
- aiosolaredge_one-0.2.0/src/aiosolaredge_one/py.typed +0 -0
- aiosolaredge_one-0.2.0/src/aiosolaredge_one/ratelimit.py +93 -0
- aiosolaredge_one-0.2.0/tests/conftest.py +71 -0
- aiosolaredge_one-0.2.0/tests/fixtures/alerts.json +1 -0
- aiosolaredge_one-0.2.0/tests/fixtures/devices.json +18 -0
- aiosolaredge_one-0.2.0/tests/fixtures/energy.json +286 -0
- aiosolaredge_one-0.2.0/tests/fixtures/overview.json +17 -0
- aiosolaredge_one-0.2.0/tests/fixtures/power.json +286 -0
- aiosolaredge_one-0.2.0/tests/fixtures/sites.json +16 -0
- aiosolaredge_one-0.2.0/tests/test_budget.py +136 -0
- aiosolaredge_one-0.2.0/tests/test_client.py +141 -0
- aiosolaredge_one-0.2.0/tests/test_ratelimit.py +60 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
concurrency:
|
|
9
|
+
group: ci-${{ github.ref }}
|
|
10
|
+
cancel-in-progress: true
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
library:
|
|
14
|
+
name: Library (aiosolaredge-one)
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: "3.13"
|
|
21
|
+
- run: pip install -e ".[dev]"
|
|
22
|
+
- name: Ruff
|
|
23
|
+
run: ruff check src tests
|
|
24
|
+
- name: Mypy (strict)
|
|
25
|
+
run: mypy src
|
|
26
|
+
- name: Pytest
|
|
27
|
+
run: pytest -q
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
name: Publish aiosolaredge-one
|
|
2
|
+
|
|
3
|
+
# Trusted Publishing (OIDC) — no API tokens stored anywhere.
|
|
4
|
+
# * Manual run (workflow_dispatch) -> TestPyPI (environment: testpypi)
|
|
5
|
+
# * GitHub Release published -> PyPI (environment: pypi)
|
|
6
|
+
# One-time PyPI/TestPyPI setup is documented in RELEASING.md.
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
workflow_dispatch:
|
|
10
|
+
release:
|
|
11
|
+
types: [published]
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
build:
|
|
15
|
+
name: Build distributions
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
- uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: "3.13"
|
|
22
|
+
- run: pip install build
|
|
23
|
+
- run: python -m build
|
|
24
|
+
- name: Validate metadata
|
|
25
|
+
run: pip install twine && python -m twine check dist/*
|
|
26
|
+
- uses: actions/upload-artifact@v4
|
|
27
|
+
with:
|
|
28
|
+
name: dist
|
|
29
|
+
path: dist/
|
|
30
|
+
|
|
31
|
+
testpypi:
|
|
32
|
+
name: Publish to TestPyPI
|
|
33
|
+
if: github.event_name == 'workflow_dispatch'
|
|
34
|
+
needs: build
|
|
35
|
+
runs-on: ubuntu-latest
|
|
36
|
+
environment: testpypi
|
|
37
|
+
permissions:
|
|
38
|
+
id-token: write
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/download-artifact@v4
|
|
41
|
+
with:
|
|
42
|
+
name: dist
|
|
43
|
+
path: dist/
|
|
44
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
45
|
+
with:
|
|
46
|
+
repository-url: https://test.pypi.org/legacy/
|
|
47
|
+
|
|
48
|
+
pypi:
|
|
49
|
+
name: Publish to PyPI
|
|
50
|
+
if: github.event_name == 'release'
|
|
51
|
+
needs: build
|
|
52
|
+
runs-on: ubuntu-latest
|
|
53
|
+
environment: pypi
|
|
54
|
+
permissions:
|
|
55
|
+
id-token: write
|
|
56
|
+
steps:
|
|
57
|
+
- uses: actions/download-artifact@v4
|
|
58
|
+
with:
|
|
59
|
+
name: dist
|
|
60
|
+
path: dist/
|
|
61
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Secrets / local env
|
|
2
|
+
.env
|
|
3
|
+
*.env
|
|
4
|
+
|
|
5
|
+
# Claude Code local settings — may contain secrets in the permission allow-list
|
|
6
|
+
.claude/settings.local.json
|
|
7
|
+
|
|
8
|
+
# Unredacted API captures — never commit
|
|
9
|
+
fixtures/_raw/
|
|
10
|
+
|
|
11
|
+
# Python
|
|
12
|
+
.venv/
|
|
13
|
+
.venv-test/
|
|
14
|
+
*.egg-info/
|
|
15
|
+
dist/
|
|
16
|
+
build/
|
|
17
|
+
__pycache__/
|
|
18
|
+
*.pyc
|
|
19
|
+
.pytest_cache/
|
|
20
|
+
.mypy_cache/
|
|
21
|
+
.ruff_cache/
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `aiosolaredge-one` are documented here. The format is
|
|
4
|
+
based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this
|
|
5
|
+
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [0.2.0] - 2026-08-24
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- `budget` module: credit-budget pacing math (`compute_interval`,
|
|
11
|
+
`plan_interval`, `BudgetPlan`, `project_month_end_usage`, `backoff_interval`,
|
|
12
|
+
`cpm_floor`) for remaining-budget-aware, self-correcting poll scheduling.
|
|
13
|
+
|
|
14
|
+
## [0.1.0] - 2026-08-24
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- Initial async client for the SolarEdge ONE (Monitoring v2) API:
|
|
18
|
+
`get_sites`, `get_site_overview`, `get_devices`, `get_energy`, `get_power`,
|
|
19
|
+
`get_alerts`, `validate`.
|
|
20
|
+
- Typed models (`Site`, `SiteOverview`, `Device`, `TimeSeries`, …), typed
|
|
21
|
+
exceptions, `CreditLedger` local monthly accounting, and `RateLimit` header
|
|
22
|
+
parsing.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 aiosolaredge-one contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: aiosolaredge-one
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Async client for the SolarEdge ONE (Monitoring v2) API
|
|
5
|
+
Project-URL: Homepage, https://github.com/CathalOConnorRH/solaredge-v2
|
|
6
|
+
Project-URL: Repository, https://github.com/CathalOConnorRH/solaredge-v2
|
|
7
|
+
Project-URL: Issues, https://github.com/CathalOConnorRH/solaredge-v2/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/CathalOConnorRH/solaredge-v2/blob/main/CHANGELOG.md
|
|
9
|
+
Author: aiosolaredge-one contributors
|
|
10
|
+
License: MIT License
|
|
11
|
+
|
|
12
|
+
Copyright (c) 2026 aiosolaredge-one contributors
|
|
13
|
+
|
|
14
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
15
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
16
|
+
in the Software without restriction, including without limitation the rights
|
|
17
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
18
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
19
|
+
furnished to do so, subject to the following conditions:
|
|
20
|
+
|
|
21
|
+
The above copyright notice and this permission notice shall be included in all
|
|
22
|
+
copies or substantial portions of the Software.
|
|
23
|
+
|
|
24
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
25
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
26
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
27
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
28
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
29
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
30
|
+
SOFTWARE.
|
|
31
|
+
License-File: LICENSE
|
|
32
|
+
Keywords: async,home-assistant,monitoring,solar,solaredge
|
|
33
|
+
Classifier: Development Status :: 3 - Alpha
|
|
34
|
+
Classifier: Framework :: AsyncIO
|
|
35
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
39
|
+
Classifier: Typing :: Typed
|
|
40
|
+
Requires-Python: >=3.11
|
|
41
|
+
Requires-Dist: aiohttp>=3.9
|
|
42
|
+
Provides-Extra: dev
|
|
43
|
+
Requires-Dist: mypy>=1.8; extra == 'dev'
|
|
44
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
45
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
46
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
47
|
+
Provides-Extra: publish
|
|
48
|
+
Requires-Dist: build>=1.2; extra == 'publish'
|
|
49
|
+
Requires-Dist: twine>=5.0; extra == 'publish'
|
|
50
|
+
Description-Content-Type: text/markdown
|
|
51
|
+
|
|
52
|
+
# aiosolaredge-one
|
|
53
|
+
|
|
54
|
+
Async Python client for the **SolarEdge ONE** (Monitoring API v2) platform. It
|
|
55
|
+
powers the
|
|
56
|
+
[SolarEdge ONE Home Assistant integration](https://github.com/CathalOConnorRH/ha-solaredge-one),
|
|
57
|
+
which lives in its own repo and depends on this package.
|
|
58
|
+
|
|
59
|
+
Base URL: `https://monitoringapi.solaredge.com/v2` · Auth: `X-API-Key` header
|
|
60
|
+
(plus optional `X-Account-Key` for Fleet accounts).
|
|
61
|
+
|
|
62
|
+
> Status: **alpha**. Endpoint coverage and models are derived from Phase 0 live
|
|
63
|
+
> captures. Some query-param names and the storage/battery/environmental paths
|
|
64
|
+
> are still unconfirmed pending docs.
|
|
65
|
+
|
|
66
|
+
## Install (dev)
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
python3 -m venv .venv && source .venv/bin/activate
|
|
70
|
+
pip install -e ".[dev]"
|
|
71
|
+
pytest
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Usage
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
import aiohttp
|
|
78
|
+
from aiosolaredge_one import SolarEdgeOneClient
|
|
79
|
+
|
|
80
|
+
async with aiohttp.ClientSession() as session:
|
|
81
|
+
client = SolarEdgeOneClient(session, api_key="...")
|
|
82
|
+
sites = await client.get_sites()
|
|
83
|
+
overview = await client.get_site_overview(sites[0].site_id)
|
|
84
|
+
print(overview.production.total, overview.production.unit)
|
|
85
|
+
power = await client.get_power(sites[0].site_id)
|
|
86
|
+
print("current power:", power.latest_value, power.unit)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## What it covers (Phase 0-confirmed endpoints)
|
|
90
|
+
|
|
91
|
+
| Method | Endpoint |
|
|
92
|
+
|--------|----------|
|
|
93
|
+
| `get_sites()` | `GET /sites` |
|
|
94
|
+
| `get_site_overview(id)` | `GET /sites/{id}/overview` |
|
|
95
|
+
| `get_devices(id)` | `GET /sites/{id}/devices` |
|
|
96
|
+
| `get_energy(id, ...)` | `GET /sites/{id}/energy` |
|
|
97
|
+
| `get_power(id, ...)` | `GET /sites/{id}/power` |
|
|
98
|
+
| `get_alerts(id)` | `GET /sites/{id}/alerts` |
|
|
99
|
+
|
|
100
|
+
Rate limiting: the client exposes `client.rate_limit` (per-minute limit +
|
|
101
|
+
remaining, parsed from response headers) and an optional `CreditLedger` for
|
|
102
|
+
tracking the monthly quota locally (not exposed by the API).
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# aiosolaredge-one
|
|
2
|
+
|
|
3
|
+
Async Python client for the **SolarEdge ONE** (Monitoring API v2) platform. It
|
|
4
|
+
powers the
|
|
5
|
+
[SolarEdge ONE Home Assistant integration](https://github.com/CathalOConnorRH/ha-solaredge-one),
|
|
6
|
+
which lives in its own repo and depends on this package.
|
|
7
|
+
|
|
8
|
+
Base URL: `https://monitoringapi.solaredge.com/v2` · Auth: `X-API-Key` header
|
|
9
|
+
(plus optional `X-Account-Key` for Fleet accounts).
|
|
10
|
+
|
|
11
|
+
> Status: **alpha**. Endpoint coverage and models are derived from Phase 0 live
|
|
12
|
+
> captures. Some query-param names and the storage/battery/environmental paths
|
|
13
|
+
> are still unconfirmed pending docs.
|
|
14
|
+
|
|
15
|
+
## Install (dev)
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
python3 -m venv .venv && source .venv/bin/activate
|
|
19
|
+
pip install -e ".[dev]"
|
|
20
|
+
pytest
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
import aiohttp
|
|
27
|
+
from aiosolaredge_one import SolarEdgeOneClient
|
|
28
|
+
|
|
29
|
+
async with aiohttp.ClientSession() as session:
|
|
30
|
+
client = SolarEdgeOneClient(session, api_key="...")
|
|
31
|
+
sites = await client.get_sites()
|
|
32
|
+
overview = await client.get_site_overview(sites[0].site_id)
|
|
33
|
+
print(overview.production.total, overview.production.unit)
|
|
34
|
+
power = await client.get_power(sites[0].site_id)
|
|
35
|
+
print("current power:", power.latest_value, power.unit)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## What it covers (Phase 0-confirmed endpoints)
|
|
39
|
+
|
|
40
|
+
| Method | Endpoint |
|
|
41
|
+
|--------|----------|
|
|
42
|
+
| `get_sites()` | `GET /sites` |
|
|
43
|
+
| `get_site_overview(id)` | `GET /sites/{id}/overview` |
|
|
44
|
+
| `get_devices(id)` | `GET /sites/{id}/devices` |
|
|
45
|
+
| `get_energy(id, ...)` | `GET /sites/{id}/energy` |
|
|
46
|
+
| `get_power(id, ...)` | `GET /sites/{id}/power` |
|
|
47
|
+
| `get_alerts(id)` | `GET /sites/{id}/alerts` |
|
|
48
|
+
|
|
49
|
+
Rate limiting: the client exposes `client.rate_limit` (per-minute limit +
|
|
50
|
+
remaining, parsed from response headers) and an optional `CreditLedger` for
|
|
51
|
+
tracking the monthly quota locally (not exposed by the API).
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Releasing `aiosolaredge-one`
|
|
2
|
+
|
|
3
|
+
This package is published to PyPI with **Trusted Publishing** (OIDC) — GitHub
|
|
4
|
+
Actions authenticates to PyPI directly, so there are **no API tokens** stored in
|
|
5
|
+
the repo or in GitHub secrets.
|
|
6
|
+
|
|
7
|
+
Workflow: [`.github/workflows/publish.yml`](.github/workflows/publish.yml)
|
|
8
|
+
- **Manual run** (`workflow_dispatch`) → **TestPyPI** (`testpypi` environment)
|
|
9
|
+
- **GitHub Release published** → **PyPI** (`pypi` environment)
|
|
10
|
+
|
|
11
|
+
## 0. Prerequisites (once)
|
|
12
|
+
|
|
13
|
+
The repo is at **`CathalOConnorRH/solaredge-v2`** and `[project.urls]` in
|
|
14
|
+
[`pyproject.toml`](pyproject.toml) already point there. If you fork or rename,
|
|
15
|
+
update those URLs and the trusted-publisher config below to match.
|
|
16
|
+
|
|
17
|
+
## 1. One-time Trusted Publisher config
|
|
18
|
+
|
|
19
|
+
Do this on **both** TestPyPI and PyPI. You do **not** need to create the project
|
|
20
|
+
first — use a "pending" publisher and it's created on first upload.
|
|
21
|
+
|
|
22
|
+
### TestPyPI
|
|
23
|
+
1. Sign in at <https://test.pypi.org> → *Your account* → *Publishing*.
|
|
24
|
+
2. Under **Add a new pending publisher**, enter:
|
|
25
|
+
- **PyPI Project Name:** `aiosolaredge-one`
|
|
26
|
+
- **Owner:** `CathalOConnorRH`
|
|
27
|
+
- **Repository name:** `solaredge-v2`
|
|
28
|
+
- **Workflow name:** `publish.yml`
|
|
29
|
+
- **Environment name:** `testpypi`
|
|
30
|
+
|
|
31
|
+
### PyPI
|
|
32
|
+
Repeat at <https://pypi.org> with the **same** values except:
|
|
33
|
+
- **Environment name:** `pypi`
|
|
34
|
+
|
|
35
|
+
### GitHub environments (optional but recommended)
|
|
36
|
+
In the GitHub repo → *Settings* → *Environments*, create `testpypi` and `pypi`.
|
|
37
|
+
Add required reviewers on `pypi` if you want a manual approval gate before a real
|
|
38
|
+
release upload.
|
|
39
|
+
|
|
40
|
+
## 2. Dry run to TestPyPI
|
|
41
|
+
|
|
42
|
+
GitHub → *Actions* → **Publish aiosolaredge-one** → **Run workflow** (on `main`).
|
|
43
|
+
This builds, `twine check`s, and uploads to TestPyPI. Verify the install:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
python -m venv /tmp/veitest && /tmp/veitest/bin/pip install \
|
|
47
|
+
--index-url https://test.pypi.org/simple/ \
|
|
48
|
+
--extra-index-url https://pypi.org/simple/ \
|
|
49
|
+
aiosolaredge-one==0.2.0
|
|
50
|
+
/tmp/veitest/bin/python -c "import aiosolaredge_one, sys; print(aiosolaredge_one.__version__)"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
(The `--extra-index-url` lets TestPyPI resolve real deps like `aiohttp`.)
|
|
54
|
+
|
|
55
|
+
## 3. Release to PyPI
|
|
56
|
+
|
|
57
|
+
1. Bump `version` in `pyproject.toml` **and** `src/aiosolaredge_one/__init__.py`
|
|
58
|
+
(`__version__`), add a `CHANGELOG.md` entry, commit.
|
|
59
|
+
2. Tag and create a GitHub Release:
|
|
60
|
+
```bash
|
|
61
|
+
git tag v0.2.0 && git push origin v0.2.0
|
|
62
|
+
gh release create v0.2.0 --title v0.2.0 --notes-from-tag
|
|
63
|
+
```
|
|
64
|
+
3. Publishing the release triggers the `pypi` job automatically.
|
|
65
|
+
|
|
66
|
+
Keep the release tag (`vX.Y.Z`) in sync with the package version. The HA
|
|
67
|
+
integration lives in its own repo
|
|
68
|
+
([`ha-solaredge-one`](https://github.com/CathalOConnorRH/ha-solaredge-one)) and
|
|
69
|
+
pins `aiosolaredge-one==<version>` in its `manifest.json`; after publishing a new
|
|
70
|
+
library version, bump that pin (and the integration `version`) there so a
|
|
71
|
+
released integration always installs a published library.
|
|
72
|
+
|
|
73
|
+
## Manual fallback (if not using CI)
|
|
74
|
+
|
|
75
|
+
Trusted Publishing is preferred, but you can upload from a laptop with an API
|
|
76
|
+
token:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
python -m pip install ".[publish]"
|
|
80
|
+
rm -rf dist && python -m build && python -m twine check dist/*
|
|
81
|
+
python -m twine upload --repository testpypi dist/* # dry run
|
|
82
|
+
python -m twine upload dist/* # real PyPI
|
|
83
|
+
```
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "aiosolaredge-one"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Async client for the SolarEdge ONE (Monitoring v2) API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = { file = "LICENSE" }
|
|
12
|
+
authors = [{ name = "aiosolaredge-one contributors" }]
|
|
13
|
+
keywords = ["solaredge", "solar", "monitoring", "home-assistant", "async"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Framework :: AsyncIO",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Typing :: Typed",
|
|
22
|
+
]
|
|
23
|
+
dependencies = ["aiohttp>=3.9"]
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
dev = [
|
|
27
|
+
"pytest>=8.0",
|
|
28
|
+
"pytest-asyncio>=0.23",
|
|
29
|
+
"mypy>=1.8",
|
|
30
|
+
"ruff>=0.4",
|
|
31
|
+
]
|
|
32
|
+
publish = [
|
|
33
|
+
"build>=1.2",
|
|
34
|
+
"twine>=5.0",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://github.com/CathalOConnorRH/solaredge-v2"
|
|
39
|
+
Repository = "https://github.com/CathalOConnorRH/solaredge-v2"
|
|
40
|
+
Issues = "https://github.com/CathalOConnorRH/solaredge-v2/issues"
|
|
41
|
+
Changelog = "https://github.com/CathalOConnorRH/solaredge-v2/blob/main/CHANGELOG.md"
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.wheel]
|
|
44
|
+
packages = ["src/aiosolaredge_one"]
|
|
45
|
+
|
|
46
|
+
[tool.pytest.ini_options]
|
|
47
|
+
asyncio_mode = "auto"
|
|
48
|
+
testpaths = ["tests"]
|
|
49
|
+
|
|
50
|
+
[tool.ruff]
|
|
51
|
+
line-length = 100
|
|
52
|
+
target-version = "py311"
|
|
53
|
+
|
|
54
|
+
[tool.ruff.lint]
|
|
55
|
+
select = ["E", "F", "I", "UP", "B", "SIM"]
|
|
56
|
+
|
|
57
|
+
[tool.mypy]
|
|
58
|
+
python_version = "3.11"
|
|
59
|
+
strict = true
|
|
60
|
+
warn_unused_ignores = true
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Async client for the SolarEdge ONE (Monitoring v2) API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .budget import (
|
|
6
|
+
BudgetPlan,
|
|
7
|
+
backoff_interval,
|
|
8
|
+
compute_interval,
|
|
9
|
+
plan_interval,
|
|
10
|
+
project_month_end_usage,
|
|
11
|
+
)
|
|
12
|
+
from .client import SolarEdgeOneClient
|
|
13
|
+
from .const import (
|
|
14
|
+
DEFAULT_BASE_URL,
|
|
15
|
+
RESOLUTION_DAY,
|
|
16
|
+
RESOLUTION_HOUR,
|
|
17
|
+
RESOLUTION_QUARTER_HOUR,
|
|
18
|
+
)
|
|
19
|
+
from .exceptions import (
|
|
20
|
+
SolarEdgeApiError,
|
|
21
|
+
SolarEdgeAuthError,
|
|
22
|
+
SolarEdgeConnectionError,
|
|
23
|
+
SolarEdgeError,
|
|
24
|
+
SolarEdgeNotFoundError,
|
|
25
|
+
SolarEdgeRateLimitError,
|
|
26
|
+
)
|
|
27
|
+
from .models import (
|
|
28
|
+
ConsumptionOverview,
|
|
29
|
+
Device,
|
|
30
|
+
ProductionOverview,
|
|
31
|
+
Site,
|
|
32
|
+
SiteOverview,
|
|
33
|
+
TimeSeries,
|
|
34
|
+
TimeValue,
|
|
35
|
+
)
|
|
36
|
+
from .ratelimit import CreditLedger, RateLimit
|
|
37
|
+
|
|
38
|
+
__version__ = "0.2.0"
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"SolarEdgeOneClient",
|
|
42
|
+
"BudgetPlan",
|
|
43
|
+
"backoff_interval",
|
|
44
|
+
"compute_interval",
|
|
45
|
+
"plan_interval",
|
|
46
|
+
"project_month_end_usage",
|
|
47
|
+
"DEFAULT_BASE_URL",
|
|
48
|
+
"RESOLUTION_DAY",
|
|
49
|
+
"RESOLUTION_HOUR",
|
|
50
|
+
"RESOLUTION_QUARTER_HOUR",
|
|
51
|
+
"SolarEdgeError",
|
|
52
|
+
"SolarEdgeApiError",
|
|
53
|
+
"SolarEdgeAuthError",
|
|
54
|
+
"SolarEdgeConnectionError",
|
|
55
|
+
"SolarEdgeNotFoundError",
|
|
56
|
+
"SolarEdgeRateLimitError",
|
|
57
|
+
"Site",
|
|
58
|
+
"SiteOverview",
|
|
59
|
+
"ProductionOverview",
|
|
60
|
+
"ConsumptionOverview",
|
|
61
|
+
"Device",
|
|
62
|
+
"TimeSeries",
|
|
63
|
+
"TimeValue",
|
|
64
|
+
"CreditLedger",
|
|
65
|
+
"RateLimit",
|
|
66
|
+
]
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Credit-budget pacing math (HA-agnostic, pure functions).
|
|
2
|
+
|
|
3
|
+
The SolarEdge ONE API exposes only a per-minute rate limit; the monthly credit
|
|
4
|
+
quota (e.g. 2000/month) is tracked locally via :class:`CreditLedger`. This module
|
|
5
|
+
turns "credits remaining this month" into "how long to wait before the next poll"
|
|
6
|
+
so a caller spends its budget evenly instead of burning it early.
|
|
7
|
+
|
|
8
|
+
The core function, :func:`compute_interval`, is *remaining-budget aware*: it
|
|
9
|
+
paces against the credits still available for the rest of the month, so it
|
|
10
|
+
self-corrects. If earlier cycles overspent, later intervals lengthen; if the
|
|
11
|
+
budget is nearly exhausted it backs off to ``max_interval``. This doubles as the
|
|
12
|
+
budget guard — no separate "are we over budget?" branch is needed for pacing.
|
|
13
|
+
|
|
14
|
+
All functions are pure (no clock, no I/O) so they can be unit-tested by feeding
|
|
15
|
+
in ``remaining_credits`` / ``seconds_until_reset`` directly.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
|
|
22
|
+
# Sensible defaults; the HA integration overrides these from config/options.
|
|
23
|
+
DEFAULT_SAFETY_FACTOR = 0.7
|
|
24
|
+
DEFAULT_NIGHT_FACTOR = 3.0
|
|
25
|
+
DEFAULT_MIN_INTERVAL = 60.0 # seconds — never poll faster than once a minute
|
|
26
|
+
DEFAULT_MAX_INTERVAL = 6 * 60 * 60.0 # seconds — poll at least every 6h
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _clamp(value: float, low: float, high: float) -> float:
|
|
30
|
+
if low > high:
|
|
31
|
+
# A pathological config (floor above ceiling); prefer respecting the
|
|
32
|
+
# per-minute floor over the ceiling to avoid violating the hard limit.
|
|
33
|
+
return low
|
|
34
|
+
return max(low, min(high, value))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def cpm_floor(credits_per_cycle: int, calls_per_minute: int) -> float:
|
|
38
|
+
"""Minimum seconds between cycles so we never exceed the per-minute limit.
|
|
39
|
+
|
|
40
|
+
A cycle issues ``credits_per_cycle`` calls; spacing cycles at least this far
|
|
41
|
+
apart keeps the average call rate at or below ``calls_per_minute``.
|
|
42
|
+
"""
|
|
43
|
+
cpm = max(1, calls_per_minute)
|
|
44
|
+
return 60.0 * max(1, credits_per_cycle) / cpm
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(slots=True, frozen=True)
|
|
48
|
+
class BudgetPlan:
|
|
49
|
+
"""User-configurable pacing parameters for one config entry."""
|
|
50
|
+
|
|
51
|
+
monthly_budget: int
|
|
52
|
+
calls_per_minute: int = 10
|
|
53
|
+
credits_per_cycle: int = 2
|
|
54
|
+
safety_factor: float = DEFAULT_SAFETY_FACTOR
|
|
55
|
+
night_factor: float = DEFAULT_NIGHT_FACTOR
|
|
56
|
+
min_interval: float = DEFAULT_MIN_INTERVAL
|
|
57
|
+
max_interval: float = DEFAULT_MAX_INTERVAL
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def target_credits(self) -> float:
|
|
61
|
+
"""Credits we actually aim to spend per month (budget × safety)."""
|
|
62
|
+
return self.monthly_budget * self.safety_factor
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def compute_interval(
|
|
66
|
+
*,
|
|
67
|
+
remaining_credits: float,
|
|
68
|
+
seconds_until_reset: float,
|
|
69
|
+
credits_per_cycle: int,
|
|
70
|
+
calls_per_minute: int,
|
|
71
|
+
min_interval: float = DEFAULT_MIN_INTERVAL,
|
|
72
|
+
max_interval: float = DEFAULT_MAX_INTERVAL,
|
|
73
|
+
is_night: bool = False,
|
|
74
|
+
night_factor: float = DEFAULT_NIGHT_FACTOR,
|
|
75
|
+
) -> float:
|
|
76
|
+
"""Seconds to wait before the next poll, paced to the remaining budget.
|
|
77
|
+
|
|
78
|
+
``remaining_credits`` should already have the safety factor applied (i.e. it
|
|
79
|
+
is the *target* remaining, not the raw quota). When it drops below one
|
|
80
|
+
cycle's cost — or the month has effectively ended — this returns
|
|
81
|
+
``max_interval`` so polling backs off instead of overspending.
|
|
82
|
+
"""
|
|
83
|
+
floor = max(min_interval, cpm_floor(credits_per_cycle, calls_per_minute))
|
|
84
|
+
if remaining_credits < credits_per_cycle or seconds_until_reset <= 0:
|
|
85
|
+
return max(max_interval, floor)
|
|
86
|
+
|
|
87
|
+
cycles_left = remaining_credits / max(1, credits_per_cycle)
|
|
88
|
+
interval = seconds_until_reset / cycles_left
|
|
89
|
+
if is_night:
|
|
90
|
+
interval *= night_factor
|
|
91
|
+
return _clamp(interval, floor, max_interval)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def plan_interval(
|
|
95
|
+
plan: BudgetPlan,
|
|
96
|
+
*,
|
|
97
|
+
used_this_month: int,
|
|
98
|
+
seconds_until_reset: float,
|
|
99
|
+
is_night: bool = False,
|
|
100
|
+
) -> float:
|
|
101
|
+
"""Convenience wrapper: interval for a :class:`BudgetPlan` given usage."""
|
|
102
|
+
remaining = max(0.0, plan.target_credits - used_this_month)
|
|
103
|
+
return compute_interval(
|
|
104
|
+
remaining_credits=remaining,
|
|
105
|
+
seconds_until_reset=seconds_until_reset,
|
|
106
|
+
credits_per_cycle=plan.credits_per_cycle,
|
|
107
|
+
calls_per_minute=plan.calls_per_minute,
|
|
108
|
+
min_interval=plan.min_interval,
|
|
109
|
+
max_interval=plan.max_interval,
|
|
110
|
+
is_night=is_night,
|
|
111
|
+
night_factor=plan.night_factor,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def project_month_end_usage(
|
|
116
|
+
*, used_this_month: int, elapsed_seconds: float, total_seconds: float
|
|
117
|
+
) -> float:
|
|
118
|
+
"""Linear projection of end-of-month spend from usage so far.
|
|
119
|
+
|
|
120
|
+
Used to decide whether to raise a "you'll blow the budget" repair issue.
|
|
121
|
+
Returns the current usage unchanged until a little time has elapsed (avoids
|
|
122
|
+
wild extrapolation from the first few seconds of the month).
|
|
123
|
+
"""
|
|
124
|
+
if elapsed_seconds <= 0 or total_seconds <= 0:
|
|
125
|
+
return float(used_this_month)
|
|
126
|
+
return used_this_month * total_seconds / elapsed_seconds
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def backoff_interval(
|
|
130
|
+
attempt: int,
|
|
131
|
+
*,
|
|
132
|
+
base: float = 60.0,
|
|
133
|
+
cap: float = 60 * 60.0,
|
|
134
|
+
retry_after: float | None = None,
|
|
135
|
+
) -> float:
|
|
136
|
+
"""Exponential backoff after a 429, honouring ``Retry-After`` when present.
|
|
137
|
+
|
|
138
|
+
``attempt`` is 1-based (first failure → ``base``). The result is capped at
|
|
139
|
+
``cap`` and never shorter than a supplied ``Retry-After``.
|
|
140
|
+
"""
|
|
141
|
+
exp = base * (2.0 ** max(0, attempt - 1))
|
|
142
|
+
interval = min(exp, cap)
|
|
143
|
+
if retry_after is not None:
|
|
144
|
+
interval = max(interval, retry_after)
|
|
145
|
+
return interval
|