ha-solis uses the SolisCloud API to fetch inverter data for photovoltaic, battery, load and grid in-feed monitoring, and exposes it to Home Assistant.
- Python 3.14+
- uv for dependency management and running code (not Poetry)
- Pydantic / Pydantic Settings for configuration and data models
- httpx (async) for HTTP, respx for mocking it in tests
- ruff for lint and format
uv run ha-solis [show|probe|dump]
uv run python -m soliscloud_api probeAlways use uv run to invoke anything — never bare python.
src/soliscloud_api/ # async API client; no Home Assistant imports
auth.py # request signing (pure functions)
client.py # SolisCloudClient, rate limiter, retry, error mapping
errors.py # one exception type per failure cause
models.py # pydantic response models
settings.py # pydantic-settings; lazy get_settings()
__main__.py # show / probe / dump CLI
custom_components/soliscloud/ # thin Home Assistant layer
scripts/build_ha.py # vendors the client into dist/soliscloud for deployment
tests/
- Use Pydantic
BaseModelfor all data structures;BaseSettingsfor config. No dataclasses. - Use
pathlib.Pathfor local file paths — neveros.path. - Never signal failure by returning an empty collection. Raise the specific
SolisErrorsubclass. An empty inverter list must mean exactly that. - Numerics default to
None, never0.0, so "not reported" stays distinguishable from "zero". - Dependency versions in
pyproject.tomlare unpinned (uv.lockpins the resolved set). Upgrade deliberately withuv lock --upgrade, then run the tests.
Measured against the live API — do not re-derive:
- Slow and lossy. Median response ~16s, tail past 30s, roughly 1 call in 4 exceeds
30s. Default timeout is 60s with up to 3 retries. A short timeout is the reason
fboundy/solis-sensorreports "No inverters found". - Rate limit: three calls per five seconds per IP.
- Units vary between fields in one response (
eTodaykWh,eTotalMWh). The<field>Strcompanion gives the unit;models.pynormalises to W and kWh. inverterDetailreturns ~812 keys. ~50 typed, the rest kept viamodel_extra.- When investigating field names, run
uv run ha-solis dumpand readdata/raw/— it writes the raw wire payload, deliberately bypassing the models.
uv run pytest # offline; live tests excluded via addopts
uv run pytest -m live # against the real API; needs .env (~5 min)tests/test_ha_component.py validates the Home Assistant layer by AST-parsing it, so
the suite runs without homeassistant installed. tests/test_ha_integration.py builds
the component and imports it against a real Home Assistant (uv sync --group ha);
skipped otherwise.
The vendored client must use relative imports only. src/soliscloud_api/ is copied
inside the component by scripts/build_ha.py, where the standalone package does not
exist. A single from soliscloud_api.x import y breaks the integration on a real HA box
while still passing locally, because the installed copy masks it. A test guards this.
uv run pre-commit install once per clone. Staged content is scanned by gitleaks
(config in .gitleaks.toml, with a custom soliscloud-key-secret rule), and a local
forbid-env-files hook rejects any staged .env because git add -f bypasses
.gitignore. Never add credentials to tests or fixtures — use the placeholder values
in .env.example.
.env in the project root provides SOLIS_ID, SOLIS_SECRET, SOLIS_URL and
optionally SOLIS_TIMEOUT. See .env.example.
Do not commit .env, and do not put coordinates or site geometry back into source:
they identify where the user lives. .gitignore matches .env at any depth and a
pre-commit hook rejects it outright.
Launch configurations in .vscode/launch.json.
Keep this file and README.md up to date as the project evolves — new modules,
dependencies, commands or structure changes should be reflected in both.