Skip to content

Add classic-auth AC charge/discharge time writes for Growatt Mix#176603

Open
albertteoh wants to merge 9 commits into
home-assistant:devfrom
albertteoh:growatt-classic-auth-writes
Open

Add classic-auth AC charge/discharge time writes for Growatt Mix#176603
albertteoh wants to merge 9 commits into
home-assistant:devfrom
albertteoh:growatt-classic-auth-writes

Conversation

@albertteoh

@albertteoh albertteoh commented Jul 16, 2026

Copy link
Copy Markdown

Why

Temporary workaround to #166817 until the token-based requests are fixed.

With token-based auth:

I used mitmproxy to see why my device (running ShinePhone) was able to update the ac charge times, and discovered it was using classic username/password auth.

I then tried to use classic username/password auth with Growatt Server on home_assistant, but when I tried growatt_server.write_ac_charge_times, I got the error: "No SPH devices with token authentication are configured. Actions require SPH devices with V1 API access."

This change allows for both growatt_server.write_ac_charge_times and growatt_server.read_ac_charge_times (and their discharge equivalents) to work with classic username/password auth on Mix devices. Reads are backed by the classic getMixSetParams endpoint (get_mix_inverter_settings) rather than the regular telemetry poll, since that poll doesn't include these settings — verified against a real Mix inverter.

Breaking change

Proposed change

Adds support for reading and writing AC charge and discharge time periods on classic (username/password) auth Growatt Mix devices, alongside the existing V1 (token) SPH read/write path.

  • coordinator: encodes charge/discharge periods as the positional param1-18 payload for update_mix_inverter_setting (classic writes), surfacing library errors, transport failures, and non-success responses as HomeAssistantError.
  • coordinator: classic reads fetch get_mix_inverter_settings (getMixSetParams) on demand and parse the same forcedChargeTime*/forcedDischargeTime*/chargePowerCommand/acChargeEnable/etc. fields the V1 SPH path already uses, plus the classic-specific wchargeSOCLowLimit2/wdisChargeSOCLowLimit2 stop-SOC fields (confirmed against a real device — the classic API exposes two SOC-limit variants per direction; 2 is the one the app displays for the AC schedule).
  • services: write handlers now match {("sph", "v1"), ("mix", "classic")} explicitly (rather than a flat device-type set) so a write can never be routed to the wrong device-specific endpoint, and always read-merge-write against real cached/fetched state for both auth types — a partial write no longer resets unspecified fields to fixed defaults.
  • services: read handlers match the same explicit pairs and now return real classic Mix settings instead of raising token_auth_required.
  • services/strings: no_devices_configured is now auth-neutral, and the write/read service descriptions mention Mix devices.
  • tests: coordinator-level unit tests for the classic read/write param encoding and error paths (including transport failures and malformed responses), plus service-layer classic-auth read/write tests.

Type of change

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New integration (thank you!)
  • New feature (which adds functionality to an existing integration)
  • Deprecation (breaking change to happen in the future)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests

Additional information

  • This PR fixes or closes issue: fixes #
  • This PR is related to issue:
  • Link to documentation pull request:
  • Link to developer documentation pull request:
  • Link to frontend pull request:

Checklist

  • I understand the code I am submitting and can explain how it works.
  • The code change is tested and works locally.
  • Local tests pass. Your PR cannot be merged unless tests pass
  • There is no commented out code in this PR.
  • I have followed the development checklist
  • I have followed the perfect PR recommendations
  • The code has been formatted using Ruff (ruff format homeassistant tests)
  • Tests have been added to verify that the new code works.
  • Any generated code has been carefully reviewed for correctness and compliance with project standards.

If user exposed functionality or configuration variables are added/changed:

If the code communicates with devices, web services, or third-party tools:

  • The manifest file has all fields filled out correctly.
    Updated and included derived files by running: python3 -m script.hassfest.
  • New or updated dependencies have been added to requirements_all.txt.
    Updated by running python3 -m script.gen_requirements_all.
  • For the updated dependencies a diff between library versions and ideally a link to the changelog/release notes is added to the PR description.

To help with the load of incoming pull requests:

Support writing AC charge and discharge time periods on classic
(username/password) auth Mix devices via the mixSet endpoint, alongside
the existing V1 (token) SPH path.

- coordinator: encode charge/discharge periods as the positional
  param1-18 payload for update_mix_inverter_setting, surfacing library
  errors and non-success responses as HomeAssistantError
- services: write handlers now match {"sph", "mix"} and branch on
  api_version; classic auth falls back to safe defaults (all periods
  off) since it has no verified live read path to merge against
- services: read handlers also match {"sph", "mix"} so classic devices
  report token_auth_required instead of no_devices_configured
- tests: add coordinator-level unit tests pinning the param encoding and
  error paths, plus service-layer classic-auth write/read tests

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@home-assistant home-assistant Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @albertteoh

It seems you haven't yet signed a CLA. Please do so here.

Once you do that we will be able to review and accept this pull request.

Thanks!

@home-assistant home-assistant Bot marked this pull request as draft July 16, 2026 10:08
@home-assistant

Copy link
Copy Markdown
Contributor

Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍

Learn more about our pull request process.

@home-assistant

Copy link
Copy Markdown
Contributor

Hey there @johanzander, mind taking a look at this pull request as it has been labeled with an integration (growatt_server) you are listed as a code owner for? Thanks!

Code owner commands

Code owners of growatt_server can trigger bot actions by commenting:

  • @home-assistant close Closes the pull request.
  • @home-assistant mark-draft Mark the pull request as draft.
  • @home-assistant ready-for-review Remove the draft status from the pull request.
  • @home-assistant rename Awesome new title Renames the pull request.
  • @home-assistant reopen Reopen the pull request.
  • @home-assistant unassign growatt_server Removes the current integration label and assignees on the pull request, add the integration domain after the command.
  • @home-assistant update-branch Update the pull request branch with the base branch.
  • @home-assistant add-label needs-more-information Add a label (needs-more-information, problem in dependency, problem in custom component, problem in config, problem in device, feature-request) to the pull request.
  • @home-assistant remove-label needs-more-information Remove a label (needs-more-information, problem in dependency, problem in custom component, problem in config, problem in device, feature-request) on the pull request.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds classic-auth Growatt Mix AC charge/discharge schedule writes alongside existing V1 SPH support.

Changes:

  • Adds classic Mix payload encoding and API calls.
  • Expands service device matching and classic defaults.
  • Adds coordinator and service tests.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
coordinator.py Implements classic schedule writes.
services.py Routes Mix devices and handles defaults.
conftest.py Mocks classic write responses.
test_coordinator.py Tests encoding and errors.
test_services.py Tests classic service behavior.

Comment thread homeassistant/components/growatt_server/services.py
Comment thread homeassistant/components/growatt_server/services.py Outdated
Comment thread homeassistant/components/growatt_server/services.py Outdated
Comment thread homeassistant/components/growatt_server/coordinator.py
Comment thread homeassistant/components/growatt_server/coordinator.py
Comment thread homeassistant/components/growatt_server/coordinator.py Outdated
Comment thread homeassistant/components/growatt_server/coordinator.py Outdated
Comment thread homeassistant/components/growatt_server/coordinator.py Outdated
Comment thread homeassistant/components/growatt_server/coordinator.py Outdated
Comment thread homeassistant/components/growatt_server/coordinator.py Outdated
Copilot AI review requested due to automatic review settings July 16, 2026 10:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 7 comments.

Comment thread homeassistant/components/growatt_server/services.py
Comment thread homeassistant/components/growatt_server/services.py Outdated
Comment thread homeassistant/components/growatt_server/services.py Outdated
Comment thread homeassistant/components/growatt_server/coordinator.py
Comment thread homeassistant/components/growatt_server/coordinator.py Outdated
Comment thread homeassistant/components/growatt_server/coordinator.py Outdated
Comment thread homeassistant/components/growatt_server/services.py
albertteoh and others added 2 commits July 16, 2026 21:25
- Add a classic-auth read path for AC charge/discharge times, parsed
  from the coordinator's already-cached mix_detail data instead of
  falling back to fixed defaults on write. Write handlers now merge
  with real cached state for both auth types, matching V1 behaviour.
- Restrict service device lookup to explicit (device_type, api_version)
  pairs (sph+v1, mix+classic) so a write can never be routed to the
  wrong device-specific endpoint.
- Require an explicit success=True in classic API responses instead of
  defaulting missing/malformed responses to success.
- Catch RequestException/JSONDecodeError around classic mixSet calls,
  not just GrowattError, so transport failures surface as
  HomeAssistantError instead of crashing the write.
- Fix the classic discharge-write path to update the coordinator cache
  on success, matching the charge-write path.
- Make the no_devices_configured message auth-neutral and update the
  write/read service descriptions to mention Mix devices.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 16, 2026 11:28
@albertteoh albertteoh force-pushed the growatt-classic-auth-writes branch from 1e22130 to 6787520 Compare July 16, 2026 11:30
@albertteoh

Copy link
Copy Markdown
Author

Pushed a follow-up commit addressing the Copilot review feedback:

  • Added a real classic-auth read path for AC charge/discharge times (parses the coordinator's already-cached mix_detail data), and switched the write handlers to always read-merge-write for both auth types — partial writes no longer reset unspecified fields to fixed defaults.
  • Restricted service device lookup to explicit (device_type, api_version) pairs so a write can't be routed to the wrong device-specific endpoint.
  • Classic API responses now require an explicit success: True; missing/malformed responses are treated as failures.
  • Broadened exception handling around the classic mixSet call to include RequestException/json.JSONDecodeError, not just GrowattError.
  • Fixed the classic discharge-write path to update the coordinator cache on success (it previously only did this for charge).
  • Made the no_devices_configured message auth-neutral and updated the write/read service descriptions to mention Mix devices.

Replied inline on each thread with details. One item I left open: the suggestion to move the param1-param18 protocol encoding into the growattServer library. I agree with the guideline it's citing, but that requires a change to the separate PyPI package plus a version bump here, which isn't something I can land in this PR — happy to follow up separately, or block on it if you'd prefer.

All tests, ruff, mypy, and pylint pass locally (164 tests in tests/components/growatt_server).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.

Comments suppressed due to low confidence (1)

homeassistant/components/growatt_server/coordinator.py:624

  • Move the classic Mix protocol implementation into growattServer and call a library helper here. This positional endpoint encoding is device protocol logic and duplicates the library's SPH implementation; Home Assistant integrations should remain thin wrappers rather than carry temporary library workarounds.
            return "00:00"
        else:
            return f"{hour:02d}:{minute:02d}"

Comment thread homeassistant/components/growatt_server/coordinator.py Outdated
Comment thread homeassistant/components/growatt_server/coordinator.py Outdated
Comment thread homeassistant/components/growatt_server/services.py
Comment thread homeassistant/components/growatt_server/services.py
Comment thread homeassistant/components/growatt_server/coordinator.py Outdated
Copilot AI review requested due to automatic review settings July 16, 2026 11:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.

Comment thread homeassistant/components/growatt_server/coordinator.py
Comment thread homeassistant/components/growatt_server/coordinator.py
Comment thread tests/components/growatt_server/conftest.py Outdated
Comment thread tests/components/growatt_server/test_coordinator.py
Comment thread tests/components/growatt_server/test_coordinator.py Outdated
Comment thread homeassistant/components/growatt_server/services.py
Copilot AI review requested due to automatic review settings July 16, 2026 11:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Comment thread homeassistant/components/growatt_server/coordinator.py Outdated
Comment thread tests/components/growatt_server/test_coordinator.py
The classic mix_detail() poll only returns telemetry (getEnergyProdAndCons_KW)
and never included the AC charge/discharge schedule fields the previous
read-path commit assumed were there - it only appeared to work because those
fields were fabricated directly into the test mock. Real settings come from a
separate endpoint, get_mix_inverter_settings() (getMixSetParams), fetched on
demand and nested under obj.mixBean.

Verified the response shape and field mapping against a real Mix inverter:
most fields (forcedChargeTime*/forcedDischargeTime*/chargePowerCommand/
acChargeEnable/etc.) match 1:1, but the stop-SOC settings have two variants
each (wchargeSOCLowLimit1/2, wdisChargeSOCLowLimit1/2); the "2" suffix is the
one the app displays for the AC charge/discharge schedule.

Also fixes a mistested classic write test that called update_ac_discharge_times
under a test named for the charge path (leaving the charge path's GrowattError
handling untested), and adds type annotations to test fixture parameters.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 16, 2026 12:05
@albertteoh

Copy link
Copy Markdown
Author

Pushed a follow-up fixing a real bug this review round caught: my previous commit's classic-auth read path assumed mix_detail() already returned the AC charge/discharge schedule fields (it doesn't — that call only returns telemetry via getEnergyProdAndCons_KW). It only appeared to work because I'd fabricated those fields directly into the test mock.

Fixed by fetching get_mix_inverter_settings() (getMixSetParams) on demand instead, and verified the actual response shape against a real Mix inverter (thanks to @albertteoh testing live). Confirmed:

  • Most fields match 1:1 with what was assumed (forcedChargeTime*/forcedDischargeTime*/chargePowerCommand/acChargeEnable/etc.), nested under obj.mixBean as Copilot suspected.
  • The stop-SOC fields have two variants each (wchargeSOCLowLimit1/2, wdisChargeSOCLowLimit1/2) — verified against the app's displayed value that 2 is the one tracking the AC charge/discharge schedule for both directions.

Also fixed a mistested classic write test (named for the charge path but calling the discharge method, leaving charge's GrowattError handling untested), added missing type annotations on test fixture parameters, and updated the PR description to match current behavior.

All 167 tests, ruff, mypy, and pylint pass locally.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Comment thread homeassistant/components/growatt_server/coordinator.py
Comment thread homeassistant/components/growatt_server/coordinator.py
The classic write path builds a fixed-length positional param list from
the caller's periods list with no length check, so a caller supplying
other than 3 periods would silently misalign every param after it.
Raise ValueError up front instead, matching the V1 SPH library's own
validation for the same constraint. Also corrected the discharge path's
comment, which claimed param1-18 (that's the charge path's count; the
discharge payload is 2 header params + 15 period params = param1-17).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 16, 2026 12:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Comment thread homeassistant/components/growatt_server/coordinator.py Outdated
get_mix_inverter_settings() failure responses (e.g. {"success": false})
have no obj.mixBean, which previously fell through to {} and was read
as all-zero/disabled settings. Combined with the write handlers'
read-merge-write pattern, a failed settings fetch could silently merge
in 0/100/disabled defaults and overwrite every field the caller didn't
specify, instead of aborting the write.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 16, 2026 12:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings July 16, 2026 12:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@albertteoh albertteoh marked this pull request as ready for review July 16, 2026 12:44
@home-assistant home-assistant Bot dismissed their stale review July 16, 2026 12:44

Stale

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants