Python library for local Modbus TCP communication with GivEnergy inverters, with no dependency on the GivEnergy Cloud. Implements a custom framer, decoder, and PDUs specific to GivEnergy's Modbus variant from scratch. Async client, pydantic v2 data models, conventional-commit changelog.
This agent owns givenergy-modbus only. Two sibling repositories exist, each with their own dedicated agent:
givenergy-hass— the Home Assistant custom component to control a local plantgivenergy-cli— a command-line interface to perform common interactive tasks
When a change in this component requires corresponding work in a sister repo, file a GitHub issue on that repo describing the API-boundary outcome the change depends on (durable contract). Ephemeral in-flight coordination between the maintainer's local agents is handled out-of-band via their agent config, not through this repo.
GitHub interactions split into two identities. The rule of thumb: anything that publishes prose as the user goes out under their keyring auth; mechanical, structural, and read-only actions go out under the automation bot.
- Git pushes are unaffected — git uses SSH, so commits/pushes always go under the
user's key regardless of token. This split only governs
gh/gh apicalls. ghtoken precedence isGH_TOKEN>GITHUB_TOKEN> keyring.- Bot identity is
dewet22-claude; the user's keyring identity isdewet22.
Bot identity (autonomous) — prefix the command with ghbot (a shell function that
loads the bot GH_TOKEN in a subshell), e.g. ghbot pr merge 35. Covers:
- All reads:
gh pr checks/view/list/diff,gh run list/view/watch,gh apiGETs,gh issue/release/repo view/list,gh search gh workflow run(release trigger)gh pr merge- Resolving review threads (GraphQL
resolveReviewThread) - Labels (
gh label,gh pr edit --add-label)
Your voice (the user) — just plain gh …. Since GH_TOKEN is never ambient, bare
gh already pins to the user's keyring identity — the default, no prefix needed.
(Belt-and-braces: env -u GH_TOKEN -u GITHUB_TOKEN gh … forces keyring even if a token
leaked into the environment.) Covers anything that authors prose as the user:
- Review-thread replies (
gh api …/comments/…/replies) gh pr comment/gh issue comment- PR review submissions (
gh pr review) gh pr create(title/body are prose authored as the user)gh issue create, closing with a comment- Editing PR/issue descriptions
The bot token lives at $CLAUDE_CONFIG_DIR/gh-env (export GH_TOKEN=…), wrapped by the
ghbot shell function (defined in ~/.zshenv so non-interactive login shells pick it up).
It needs repo (read, merge, labels) and workflow (trigger releases) scopes. This is a
shared cross-agent convention — the modbus and hass agents follow the same split.
givenergy_modbus/client/client.py—Client: primary public interfacegivenergy_modbus/client/commands.py—RegisterMapand high-level command methods (allset_*controls / write commands)givenergy_modbus/model/— inverter/battery data models and register definitionsinverter.py—SinglePhaseInverter,SlotMap,SINGLE_PHASE_SLOTS,EXTENDED_SLOTSinverter_threephase.py—ThreePhaseInverter,THREE_PHASE_SLOTS
givenergy_modbus/pdu/— Protocol Data Units (custom GivEnergy framing)write_registers.py—WRITE_SAFE_REGISTERSallowlist (see Critical Caution)
givenergy_modbus/framer.py— custom Modbus framer (GivEnergy wire format)givenergy_modbus/codec.py— decoder for PDU payloadsdocs/usage.md— user-facing command reference (keep in sync with commands.py)
Prefer real evidence (wire captures, GivTCP cross-reference) before adding a writable register. A new writable register must be added in two places or it fails at send time:
- the model-aware
WRITE_SAFE_*set inmodel/manifest.py(Gate 1 — the client-boundary check viawrite_safe_registers(); add to the set matching the models that physically accept the register:WRITE_SAFE_SINGLE_PHASE/_THREE_PHASE/_EMS/_AC_CONFIG), and - the canonical
WRITE_SAFE_REGISTERSinpdu/write_registers.py(Gate 2) — enforced byWriteHoldingRegisterRequest.ensure_valid_state()atencode()time. A register missing here builds a request fine but raisesInvalidPduStateon the wire.
Add a regression test that calls .encode() on the new command (not just constructs
it) — constructing alone won't catch a missing PDU-allowlist entry.
Slot availability is model-dependent. SinglePhaseInverter.slot_map returns either
SINGLE_PHASE_SLOTS (2 slots) or EXTENDED_SLOTS (10 slots) based on DTC + ARM
firmware version. Always pass inverter.slot_map to slot commands; never hardcode
register addresses.
When adding, removing, or changing any function in client/commands.py, update
docs/usage.md in the same commit. That commands table is the primary reference for
downstream consumers.
Diagrams under docs/img/ are committed as both an editable .svg source and a
rendered .png (the docs embed the PNG; there is no CI rasterisation step). The SVGs
are standalone — hardcoded hex palette and explicit fonts, no external CSS — so they
render the same in a browser, on GitHub, or via rsvg-convert. After editing an SVG,
re-render its PNG at 2× and commit both:
rsvg-convert -w 1360 docs/img/<name>.svg -o docs/img/<name>.png
- Inclusive terminology only:
device_address, neverslave_address; no master/slave in code, comments, commit messages, or docs (quote legacy protocol terms verbatim, in a quote block, if unavoidable). - Conventional commits:
feat:,fix:,refactor:, etc.
Run tox before every commit, not just before a PR. The CI matrix runs the same
envs; pytest alone misses things CI catches in ~20s of local tox:
lintruns mypy, which spots mixin-pattern cross-class invariants (e.g. a mixin readingself.slot_mapwhen the attribute lives on the composed inverter class) that pytest can't see.formatrunsruff format --check, which catches preferred-syntax drift — notably PEP 758 unparenthesisedexceptclauses on the py314 target.buildrunsmkdocs build,twine check, and the wheel build, where docstring problems, broken nav links, and packaging regressions surface before a release.
uv run --group test tox # full check — pytest + ruff + mypy + bandit + build + docs
uv run --group test pytest # faster: tests only
uv run ruff check --fix && uv run ruff format # faster: format + lint onlyClientis the primary public interface — backwards compatibility matters.set_*methods incommands.pyexpose inverter control; treat with extra care.- This library is consumed by givenergy-hass (Home Assistant integration) and givenergy-cli, each maintained separately. Keep public API changes conservative and backwards-compatible where possible. Cross-repo changes are coordinated via self-contained handoff specs that state the expected outcomes at the API boundary without prescribing the consumer's implementation.
- Public API changes require a CHANGELOG entry (see below).
CHANGELOG.md is generated at release time by scripts/release.py generate, which
walks git log <last-v-tag>..HEAD on the current branch and writes a new versioned
section. There is no [Unreleased] section between releases. The conventional-commit
prefix determines the section:
| Prefix | Section |
|---|---|
feat: |
✨ Added |
fix: / revert: |
🐛 Fixed |
perf: / <type>!: (breaking) |
🔄 Changed |
security: |
🔒 Security |
refactor: / docs: / chore: / ci: / test: / style: / build: / wip: |
🔧 Maintenance |
Default rule: don't edit CHANGELOG.md directly. Let release.py generate build
it from commits at release time. Editing by hand is reserved for fixing past mistakes
in already-released sections; the most-recent section is rewritten on the next release
if you touch it.
When the conventional-commit prefix doesn't reflect the user impact, add a Changelog:
git trailer to the commit body:
refactor: rename a public model field for clarity
Changelog: Changed
Changelog: <section>redirects the entry to that section (case-insensitive; matchesAdded,Changed,Deprecated,Removed,Fixed,Security,Maintenance).Changelog: skipsuppresses the entry entirely. Useful for fixup commits whose narrative is already captured by their parent.
Trailer must live in the final paragraph of the commit body (standard git trailer
semantics). Last Changelog: trailer in the final paragraph wins.
python3 scripts/release.py generate <next-version> --previewWalks the same commits and prints the rendered section without modifying the file.
git log <last-v-tag>..HEAD --oneline is the lighter-weight alternative.
pydantic— data models and validationcrccheck— CRC computation for frame integrity- Apache-2.0 licensed, published to PyPI