Skip to content

fix(inverter): auto-create charge_rate entity for script-driven "power" inverters (#3311) - #4645

Closed
chalfontchubby wants to merge 4 commits into
mainfrom
fix/solax-low-power-charge-rate-entity
Closed

chalfontchubby wants to merge 4 commits into
mainfrom
fix/solax-low-power-charge-rate-entity

Conversation

@chalfontchubby

@chalfontchubby chalfontchubby commented Aug 22, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Fixes #3311 - Solax low power mode sends battery_rate_max instead of the planned rate to charge_start_service.

Root cause. output_charge_control: "power" inverters normally write their rate straight to the inverter through an integration (GivTCP REST, a cloud component), so the dummy charge_rate/discharge_rate entities were only ever auto-created for "current" mode. Solax is "power" mode but script-driven: charge_start_service calls a plain HA script, and get_current_charge_rate() / adjust_charge_rate() go through args["charge_rate"] exactly as "current" mode does. With no entity, the low-power calculation runs (the reporter's log shows "Best rate: 2500W") but has nowhere to store the result, so it reads back as battery_rate_max and the script gets full power. Community member f948lan found the same root cause and worked around it with input_number helpers - this makes that automatic.

Change

  • A new has_charge_rate_entity inverter-definition flag (default True) declares whether a "power" type needs the entity. The Solax template states it in its own inverter: block, and it is documented with the other definition keys.
  • The entity is created per inverter, only where that inverter's slot is genuinely empty:
    • a configured entity, or a single non-list value, is never replaced (a bare number in a list slot - another inverter's defaults, or a number typed into a charge_rate list - is not an entity, so it is filled; on main such a number was read as a fixed rate and every write to it failed);
    • a slot whose charge_rate_percent / discharge_rate_percent is configured is left alone - the percentage register is the real control (GECloud 3-phase units, GEC (GivEnergy Cloud) with ge_cloud_automatic: true uses hardcoded 2600W default instead of correctly-fetched 20000W inverter capability (3-phase HV inverter) #4908);
    • when an integration is active but has not written the key at all (its automatic config is off, or it has not run yet), nothing is created, so a REST/cloud inverter's rate writes never land in a sensor nothing reads.
  • Predbat's own dummy is re-asserted on every refresh, so it comes back after an HA restart.
  • GE/GEC/GEE size the battery's maximum rate from a configured charge_rate's max attribute. Predbat's own dummy has none, so it is treated as unset there rather than dropping the maximum to 2600W.

Known limitation. A script-driven inverter alongside an integration-backed one whose automatic config is off still gets no entity (the integration has written nothing, so the third rule above applies to every inverter). Configure charge_rate for it in apps.yaml in that case. (Conversely, if a "current" mode inverter earlier in the fleet has already seeded the list, an integration-backed inverter with automatic config off does get a dummy - its rate writes were rejected as a fixed value before, so this only changes how that misconfiguration fails.)

Separate, pre-existing issue. Since the balance fold into execute.py, execute_plan() writes rates in apply_rate_intent() after its per-inverter loop, while adjust_charge_immediate() / adjust_export_immediate() build {power} inside the loop. So a service-driven inverter receives the previous cycle's rate: the first start call of a low-power window still carries the maximum, corrected one cycle later. That affects every service-driven inverter (including the #4619 discharge path), not just this change - tracked in #5252.

Rebased onto current main as a single commit plus the review fixes; the block now runs in refresh_config(), i.e. every plan cycle since #5126 made inverters persist, and is idempotent there.

Test plan

New tests in test_inverter.py:

  • entity created for a source-less "power" inverter; not created for an integration-backed one
  • a pre-configured charge_rate is not replaced
  • partial multi-inverter list: slot 1 filled, slot 0 untouched; same beyond an integration's own fleet
  • two source-less inverters from an absent key each get their own entity, and inverter 1's planned rate round-trips
  • no entity for a slot with percentage control configured
  • battery maximum stays at battery_rate_max across a refresh and a fresh build once the dummy exists
  • on a user-defined Solax type (the Low power mode does not work on Solax #3311 setup), with the rate set first: charge_start_service receives the planned 2500W as {power}; with the flag off it receives battery_rate_max
  • the dummy is re-created after HA loses it, and a fresh object knows its attributes
  • a bare-number slot is filled
  • each review-round regression test fails without its fix
  • ./run_all --quick and ./run_pre_commit clean

Follow-up

Mixed setups, where one inverter uses charge_rate_percent and another a script-driven {power}, are tracked in #5359.
The rate readers and writers still choose their path by whether the key exists, not by each inverter's own entry.
That was already wrong on main for the Solax side. This PR adds one new warning in one of those setups, described in the issue.

🤖 Generated with Claude Code

Comment thread apps/predbat/inverter.py Outdated
@chalfontchubby chalfontchubby added the BOT_CLEANUP Trigger: bot should address PR review feedback and CI failures, then commit and push label Sep 7, 2026
@springfall2008
springfall2008 requested a lite review from Copilot September 7, 2026 10:24

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.

🔵 Needs a closer look

The new REST-less "power" auto-creation logic only checks for key presence, which can miss per-inverter missing/None entries and still fall back to max rate in multi-inverter/partial-list configurations.

Pull request overview

This PR fixes #3311 by ensuring script/service-driven inverters that use output_charge_control: "power" but don’t have REST data can still persist and read back the computed low-power charge/discharge rate via auto-created synthetic charge_rate / discharge_rate entities.

Changes:

  • Add a per-inverter-type flag (has_charge_rate_entity) and use it to conditionally auto-create synthetic charge_rate/discharge_rate entities for REST-less "power" inverters.
  • Add inverter tests covering creation, non-clobbering when already configured, and no creation for REST-driven "power" inverters.
  • Add the new flag to the Solax Gen4 (SX4) inverter definition.
File summaries
File Description
apps/predbat/inverter.py Adds conditional synthetic entity auto-creation for REST-less "power" inverters and introduces a per-type flag to control it.
apps/predbat/config.py Adds has_charge_rate_entity to the SX4 inverter definition to document/declare the intended behavior.
apps/predbat/tests/test_inverter.py Adds regression tests for entity creation, non-clobbering, and REST-driven exclusion.
Review details

Suppressed comments (1)

apps/predbat/inverter.py:588

  • The REST-less "power" branch only auto-creates charge_rate/discharge_rate when the key is entirely absent. If the key exists but this inverter’s slot is missing/None (e.g., shorter list in multi-inverter configs), get_current_charge_rate() will still resolve to None and fall back to battery_rate_max_raw, recreating the #3311 behaviour for that inverter. Consider treating the arg as "not configured" when the per-inverter index is out of range or None, while still not clobbering scalar (single-value) configs.
            self.create_missing_arg("idle_start_time", "00:00:00")
            self.create_missing_arg("idle_end_time", "00:00:00")
            self.base.args["idle_start_time"][id] = self.create_entity("idle_start_time", "00:00:00")
            self.base.args["idle_end_time"][id] = self.create_entity("idle_end_time", "00:00:00")

  • Files reviewed: 3/3 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@springfall2008 springfall2008 removed the BOT_CLEANUP Trigger: bot should address PR review feedback and CI failures, then commit and push label Sep 7, 2026
@chalfontchubby

chalfontchubby commented Sep 7, 2026 •

Copy link
Copy Markdown
Collaborator Author

Posted by Claude on behalf of @chalfontchubby.

Addressed the Copilot review's suppressed finding on inverter.py (multi-inverter partial list) in 1206e55.

The auto-creation branch gated on "charge_rate" not in self.base.args, so a mixed multi-inverter config where charge_rate is a list shorter than the inverter count — or has a None in one slot — would skip creation for that inverter, and get_current_charge_rate() would still fall back to battery_rate_max_raw (the #3311 fault) for a source-less "power" inverter in a later slot.

Now gated per index: create the synthetic entity when this inverter's slot is absent or None, extending the list if it's short, while still never overwriting a real per-inverter value or a single non-list value that applies to every inverter. charge_rate/discharge_rate folded into one loop. New test test_low_power_mode_entity_filled_for_partial_multi_inverter_list covers a two-inverter "power" config with only inverter 0 configured. run_pre_commit and the inverter test module pass on the branch.

chalfontchubby added a commit that referenced this pull request Sep 7, 2026
Copilot review on #4645: gating auto-creation on `"charge_rate" not in
self.base.args` misses a mixed multi-inverter config where charge_rate is
a list shorter than the inverter count, or carries a None in one
inverter's slot. The key is present, so the old guard skipped creation,
and get_current_charge_rate() for that inverter still resolved to
battery_rate_max_raw - the #3311 fault - for a source-less "power"
inverter in a later slot.

Gate per index instead: create the synthetic entity when this inverter's
slot is absent or None, extending the list if it is short, while still
never overwriting a real per-inverter value or a single (non-list) user
value that applies to every inverter. Folds the charge_rate/discharge_rate
pair into one loop.

Test: a two-inverter "power" config with charge_rate configured for
inverter 0 only now auto-fills inverter 1's slot and leaves inverter 0's
entity untouched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@chalfontchubby
chalfontchubby force-pushed the fix/solax-low-power-charge-rate-entity branch from c68e38a to 1206e55 Compare September 7, 2026 11:10
@chalfontchubby
chalfontchubby requested a lite review from Copilot September 11, 2026 21: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.

🟡 Changes recommended

Two moderate findings remain unresolved.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (3)

apps/predbat/config.py:1952

  • This new SX4/Solax definition entry is not exercised by the added tests: every fixture sets inverter_type to GE. Because Inverter.__init__ currently defaults has_charge_rate_entity to True, the suite would still pass if this line were absent or attached to the wrong inverter definition. Add a case using SX4 (or the custom Solax definition) and verify the resulting service payload, not only the generic GE branch.
        "has_charge_rate_entity": True,

apps/predbat/inverter.py:431

  • This newly assigned attribute is never read anywhere in the repository, and no INVERTER_DEF entry still declares has_fox_inverter_mode. The Fox mode handling was removed, so retaining this dead capability field adds misleading state and makes the inverter definition harder to reason about; remove the stale assignment.
        self.inv_has_fox_inverter_mode = INVERTER_DEF[self.inverter_type].get("has_fox_inverter_mode", False)

apps/predbat/tests/test_inverter.py:793

  • This regression test only verifies that the charge_rate/discharge_rate keys exist; it never writes a planned low-power value or calls adjust_charge_immediate(). It would still pass if get_current_charge_rate() or the charge_start_service payload continued to use battery_rate_max_raw, so add a Solax/SX4-shaped assertion that the service receives a value such as 2500 W.
        if "charge_rate" not in my_predbat.args:
            print("ERROR: {} charge_rate entity was not auto-created for a source-less 'power' inverter".format(test_name))
            failed = True
        if "discharge_rate" not in my_predbat.args:
            print("ERROR: {} discharge_rate entity was not auto-created for a source-less 'power' inverter".format(test_name))
  • Files reviewed: 3/3 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread apps/predbat/inverter.py Outdated
Comment thread apps/predbat/tests/test_inverter.py Outdated
@chalfontchubby chalfontchubby added the BOT_CLEANUP Trigger: bot should address PR review feedback and CI failures, then commit and push label Sep 12, 2026
@springfall2008 springfall2008 removed the BOT_CLEANUP Trigger: bot should address PR review feedback and CI failures, then commit and push label Sep 12, 2026
chalfontchubby added a commit that referenced this pull request Sep 13, 2026
…st args

Review feedback on #4645: constructing an Inverter writes a dummy entity
into args for every register the type lacks AND creates the matching state
in the shared HA interface. The tests snapshotted args alone, so the entity
state leaked into every test running after them in this module - measured
at 12 entities, including the sensor.predbat_GE_1_* set the review named.

Snapshot and restore both through one helper rather than per test, and
apply it to test_short_per_inverter_list_gets_its_dummy_entity too: the
review cited it as the model to follow, but it had the same gap.

Verified by diffing the fixture's args keys and dummy_items around the
whole group - 12 leaked entities before, none after.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
springfall2008 pushed a commit that referenced this pull request Sep 26, 2026
…n adoption, split gap/tail log

- automatic_config() records what each key held before its first claim and hands a
  capability-gated key back to it when a re-run's gate fails (a late endpoint lacking v3 or a
  register), instead of leaving the gap-slot claim pointing at an entity that is never published.
- run() publishes again straight after rediscover() adopts an endpoint, so the re-run's
  discovery-key gates see the adopted inverter's sensors rather than failing for all of them.
- Gap slots below the highest answered endpoint get their own Warn line naming the URL and the
  cost (planned without live data until it answers); only tail slots are "left as configured".
- Rewrite the automatic_config() header comment to the per-endpoint invariant.
- Update the debug-journal GH#5029 row and PR #4645 proxy bullet for _per_endpoint_values().

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
chalfontchubby added a commit that referenced this pull request Sep 26, 2026
…r" inverters (#3311)

Solax's low-power-mode charge rate is driven via charge_start_service (a plain
HA script call), not a REST/cloud API, but output_charge_control: "power" meant
the dummy charge_rate/discharge_rate entities were only ever auto-created for
"current" mode. Without the entity, the computed rate had nowhere to be stored
and read back as battery_rate_max_raw, sending full power to the script
regardless of what was planned.

Whether an inverter type needs the entity is declared by a new
has_charge_rate_entity INVERTER_DEF flag (default True). Creation is gated per
inverter index rather than on key presence, never overwrites a configured
value, and is skipped when an inverter-source component is active but has not
written the key at all (its automatic config is off, or it has not run yet),
so a REST/cloud-controlled inverter's rate writes never land in a dummy.

Squashed from the review history of #4645 onto current main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
chalfontchubby added a commit that referenced this pull request Sep 26, 2026
Two cold reviews of #4645 found these ways the auto-created charge_rate
entity still failed:

- With the rate keys absent, inverter 0 seeded [its dummy, default,
  default, default]. Inverter 1 then read the bare default in its own slot
  as configured and skipped creation, so its rate writes went nowhere and
  it read back battery_rate_max - the #3311 fault for every inverter after
  the first. An absent key now starts empty and is padded with None, as
  create_missing_arg() already does for a short list; a bare number left in
  a slot by a "current" mode inverter's seeding is treated as unset too.
- Once the dummy was in args it was never created again, so after an HA
  restart the sensor stayed missing, and a fresh Inverter object never
  learnt its attributes (writes dropped the unit). Predbat's own dummy is
  now re-asserted on every refresh; create_entity() leaves existing state.
- GE/GEC/GEE size battery_rate_max_raw from a configured charge_rate's
  "max" attribute. The dummy has none, so every refresh after it existed
  (each plan cycle now inverters persist) dropped to 2600W. Predbat's own
  dummy is now treated as unset there.
- A slot whose *_rate_percent is configured (GECloud 3-phase units, #4908)
  was given a dummy it does not need, which the previous point then turned
  into a wrong battery maximum. Such a slot is now left alone.

The #3311 setup is a user-defined SOLAX type, which copies GE's definition
and never read the SX4 entry: the template now states has_charge_rate_entity
itself, and the SX4 comment no longer claims to be the originating case. New
tests check the planned rate reaches charge_start_service as {power} on that
setup (and that opting out falls back to battery_rate_max). The flag and its
mixed-fleet limitation are documented alongside the other definition keys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chalfontchubby
chalfontchubby force-pushed the fix/solax-low-power-charge-rate-entity branch from 7cb4bcc to a75b70c Compare September 26, 2026 19:39
chalfontchubby added a commit that referenced this pull request Sep 26, 2026
Two cold reviews of #4645 found these ways the auto-created charge_rate
entity still failed:

- With the rate keys absent, inverter 0 seeded [its dummy, default,
  default, default]. Inverter 1 then read the bare default in its own slot
  as configured and skipped creation, so its rate writes went nowhere and
  it read back battery_rate_max - the #3311 fault for every inverter after
  the first. An absent key now starts empty and is padded with None, as
  create_missing_arg() already does for a short list; a bare number left in
  a slot by a "current" mode inverter's seeding is treated as unset too.
- Once the dummy was in args it was never created again, so after an HA
  restart the sensor stayed missing, and a fresh Inverter object never
  learnt its attributes (writes dropped the unit). Predbat's own dummy is
  now re-asserted on every refresh; create_entity() leaves existing state.
- GE/GEC/GEE size battery_rate_max_raw from a configured charge_rate's
  "max" attribute. The dummy has none, so every refresh after it existed
  (each plan cycle now inverters persist) dropped to 2600W. Predbat's own
  dummy is now treated as unset there.
- A slot whose *_rate_percent is configured (GECloud 3-phase units, #4908)
  was given a dummy it does not need, which the previous point then turned
  into a wrong battery maximum. Such a slot is now left alone.

The #3311 setup is a user-defined SOLAX type, which copies GE's definition
and never read the SX4 entry: the template now states has_charge_rate_entity
itself, and the SX4 comment no longer claims to be the originating case. New
tests check the planned rate reaches charge_start_service as {power} on that
setup (and that opting out falls back to battery_rate_max). The flag and its
mixed-fleet limitation are documented alongside the other definition keys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chalfontchubby
chalfontchubby force-pushed the fix/solax-low-power-charge-rate-entity branch from a75b70c to 8d2714b Compare September 26, 2026 19:49
Comment thread apps/predbat/inverter.py Outdated
# object has anything but a registry that carries the method. A registry that cannot
# answer - the harness stubs one with, at most, the few lookups its own test needs - is
# treated as "no source", which is also what a None registry already meant above.
source_active = getattr(components, "inverter_source_active", None)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Why getattr() I usually don't allow them?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Posted by Claude on behalf of @chalfontchubby.

Fair point. It was only there so Inverter construction would tolerate test stubs whose registry lacked the method, and production code shouldn't bend to test doubles. Removed in 36b4ade, back to main's direct components.inverter_source_active() call. Nothing needs it any more: the full quick suite passes. I also rebased onto main to clear the merge conflict.

chalfontchubby and others added 3 commits September 29, 2026 13:19
…r" inverters (#3311)

Solax's low-power-mode charge rate is driven via charge_start_service (a plain
HA script call), not a REST/cloud API, but output_charge_control: "power" meant
the dummy charge_rate/discharge_rate entities were only ever auto-created for
"current" mode. Without the entity, the computed rate had nowhere to be stored
and read back as battery_rate_max_raw, sending full power to the script
regardless of what was planned.

Whether an inverter type needs the entity is declared by a new
has_charge_rate_entity INVERTER_DEF flag (default True). Creation is gated per
inverter index rather than on key presence, never overwrites a configured
value, and is skipped when an inverter-source component is active but has not
written the key at all (its automatic config is off, or it has not run yet),
so a REST/cloud-controlled inverter's rate writes never land in a dummy.

Squashed from the review history of #4645 onto current main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two cold reviews of #4645 found these ways the auto-created charge_rate
entity still failed:

- With the rate keys absent, inverter 0 seeded [its dummy, default,
  default, default]. Inverter 1 then read the bare default in its own slot
  as configured and skipped creation, so its rate writes went nowhere and
  it read back battery_rate_max - the #3311 fault for every inverter after
  the first. An absent key now starts empty and is padded with None, as
  create_missing_arg() already does for a short list; a bare number left in
  a slot by a "current" mode inverter's seeding is treated as unset too.
- Once the dummy was in args it was never created again, so after an HA
  restart the sensor stayed missing, and a fresh Inverter object never
  learnt its attributes (writes dropped the unit). Predbat's own dummy is
  now re-asserted on every refresh; create_entity() leaves existing state.
- GE/GEC/GEE size battery_rate_max_raw from a configured charge_rate's
  "max" attribute. The dummy has none, so every refresh after it existed
  (each plan cycle now inverters persist) dropped to 2600W. Predbat's own
  dummy is now treated as unset there.
- A slot whose *_rate_percent is configured (GECloud 3-phase units, #4908)
  was given a dummy it does not need, which the previous point then turned
  into a wrong battery maximum. Such a slot is now left alone.

The #3311 setup is a user-defined SOLAX type, which copies GE's definition
and never read the SX4 entry: the template now states has_charge_rate_entity
itself, and the SX4 comment no longer claims to be the originating case. New
tests check the planned rate reaches charge_start_service as {power} on that
setup (and that opting out falls back to battery_rate_max). The flag and its
mixed-fleet limitation are documented alongside the other definition keys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s active

Drop the getattr() around components.inverter_source_active() and go back
to main's direct call. It was added so Inverter construction would tolerate
test stubs whose registry lacked the method; no test needs that any more,
and production code shouldn't bend to an incomplete test double.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chalfontchubby
chalfontchubby force-pushed the fix/solax-low-power-charge-rate-entity branch from 8d2714b to 36b4ade Compare September 29, 2026 18:33
TestHAInterface always has dummy_items, and the rest of test_inverter.py uses it
directly, so the getattr fallbacks in the snapshot/restore helpers only hid a
broken fixture (same point as the #4645 review on inverter.py).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

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 review overview

🟡 Changes recommended

Unresolved critical and moderate review findings affect discovery compatibility and inverter rate control.

Review effort: Lite
Findings: 2 High severity · 1 Medium severity

Open (3)
Resolved since last review (2)

Comment thread apps/predbat/config.py
# Script-driven "power" mode with no REST/native register to hold the computed
# charge/discharge rate. Stated explicitly although it is the default; the Solax Gen4+
# template (templates/solax_sx4.yaml, the #3311 setup) sets it in its own inverter: block.
"has_charge_rate_entity": True,
Comment thread apps/predbat/inverter.py
Comment on lines +801 to +805
if self.base.get_arg(rate_arg + "_percent", indirect=False, index=self.id, default=None):
# The percentage register is this inverter's real rate control (GECloud's 3-phase
# units, #4908) and get_current_charge_rate() reads it in preference, so the empty
# power slot beside it is not a gap to fill.
continue
Comment thread apps/predbat/inverter.py
# inv_needs_charge_rate_entity branch below) has no "max" attribute either, so it is
# treated as unset too - otherwise every refresh after it was created fell back to 2600W.
charge_rate_entity = self.base.get_arg("charge_rate", indirect=False, index=self.id)
if charge_rate_entity and charge_rate_entity != self.dummy_entity_id("charge_rate"):
@chalfontchubby

Copy link
Copy Markdown
Collaborator Author

Posted by Claude on behalf of @chalfontchubby.

Closing in favour of #5373, which fixes #3311 a different way. Instead of creating a dummy charge_rate/discharge_rate entity, it remembers the rate Predbat last set on the inverter object and reads that back when there is no rate entity.

Most of this PR's review went into problems the dummy entity itself caused: entries in the per-inverter lists, the interaction with charge_rate_percent and components, the GivEnergy maximum-rate lookup, and a new inverter-type flag. #5373 needs none of that. The mixed-fleet follow-up stays in #5359. Thanks for the reviews here; they shaped the redo.

chalfontchubby added a commit that referenced this pull request Oct 4, 2026
…th no rate entity (#3311)

A script-driven "power" inverter (the Solax SX4 template) has no charge_rate or
discharge_rate register, so get_current_charge_rate()/get_current_discharge_rate()
fell back to battery_rate_max, and that maximum was sent to
charge_start_service/discharge_start_service as {power} however low the planned
rate was: a low-power charge ran at full power.

Predbat chooses the rate itself, so hold the last one set on the Inverter object,
which outlives the plan cycle (#5126), and read that back when a "power" inverter
that is sent {power} by a start or freeze service (services_send_power()) has
neither a rate entity nor a rate percentage (rate_without_entity()). Without those services nothing applies a
held rate, so those inverters read back the maximum as before. A plain
number in its rate entry - in a mixed fleet, another inverter's default from
create_missing_arg() - is not an entity either, and is never written to. With the
previous commit calling the start services after the rates are set, {power} is the
planned rate from the first cycle of a window. A restart reads battery_rate_max
until the next rate is set. "current" inverters already get a rate entity, and
"none" inverters are unchanged.

A rate held this way moves as a register would (rate_changed()): a change inside
the 5% deadband is held, so {power} - part of the start service's dedup - does not
re-send the script every cycle. Any move to or from 0 is a change, though: on a
10kW battery (a 500W deadband) a 400W low power charge straight after an export
would otherwise stay at the export's 0. A rate register keeps the plain deadband,
so one that stores a small rate as 0 is not rewritten every cycle.

Also fixes self_test() passing kW per minute as rates, and the swapped inverter id
and value in the "is not a number" errors.

This replaces #4645, which created a dummy HA entity for the same purpose.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Low power mode does not work on Solax

3 participants