Skip to content

feat(hanchu): Hanchu ESS cloud component (draft — no official spec, unverified against hardware) - #5305

Merged
springfall2008 merged 10 commits into
mainfrom
feat/hanchu-component
Oct 20, 2026
Merged

springfall2008 merged 10 commits into
mainfrom
feat/hanchu-component

Conversation

@mgazza

@mgazza mgazza commented Sep 29, 2026

Copy link
Copy Markdown
Collaborator

Draft — not ready to merge. Built with no official vendor specification: every endpoint and field was derived from third-party Home Assistant integrations, and nothing has run against real hardware. Two inferred behaviours would silently produce a wrong plan if they are wrong — see below.

What this adds

A native Hanchu ESS cloud component (HanchuCloud), following the AlphaESS/Sunsynk structure: login + token refresh + reactive re-login, device discovery, tiered polling, sensor publishing, and a batched control path.

File Lines
apps/predbat/hanchu.py 1,540
apps/predbat/hanchu_const.py 511
templates/hanchu.yaml 444
5 test suites (tests/test_hanchu_*.py) ~2,160, 67 cases
components.py / config.py / unit_test.py +99

All five Hanchu suites pass; pre-commit (ruff, black, cspell, yaml) clean; 100% docstrings. The existing hanchu_cloud.yaml HACS-bridge template is untouched.

The --quick suite stops later on data_age_metrics (Expected data_age_days=8, got 0). That is pre-existing — reproduced identically at base commit d951201e in a throwaway worktree.

Provenance — read this before trusting anything

There is no published Hanchu API. Sources, all MIT-licensed:

  • upton68/hanchu-ess-ha — actively maintained (~281 commits); primary source
  • guoxiatech/hanchu-ess-ha — its upstream (~128 commits)
  • Blustery7752/hanchu-ess — thinner, but talks to a different endpoint family

Evidence was graded rather than counted: guoxiatech's request layer is a near-copy of upton68's, so agreement between those two is weak. Blustery7752 is independent, so where it agrees the evidence is strong. Every item in hanchu_const.py is annotated OBSERVED / CORROBORATED / INFERRED with its source repo and file.

Licence: all code written fresh in batpred's idiom; nothing ported. Only facts about the API surface were taken.

Corroborated across independent sources: base URL iess3.hanchuess.com; iotGet/iotSet body shape {devType, sn, keys/value}; devType "2" = DTU (as a string); fastChargeDischarge semantics.

⚠️ Inferred — not read in any reference

  • Work mode 3 "User-defined" is the mode that honours the timed slots. The biggest guess. hanchu_work_mode_control: False is the escape hatch.
  • Slot start == end == 0 means disabled — no per-slot enable key exists among the twelve.
  • inverter_limit derived from the discharge-power ceiling, battery_rate_max from the charge-power ceiling — the API reports neither rating.
  • 60s default write pacing. Batching is observed (the reference stages changes and flushes once, explicitly to avoid rate limiting); no numeric limit is published anywhere.
  • The 25-day token lifetime is upton68's own safety margin, not a documented server figure.

Where the references disagreed

Point Conflict Followed
Login plaintext pwd at identify/auth/token vs Blustery's RSA-encrypted identify/auth/login/account upton68
Endpoint family app/ha/* plain JSON vs Blustery's platform/* with AES bodies app/ha/*
Reauth code upton68 contradicts itself — docstring 90076, tests use 100 the code
Timeout/retry 15s ×3 vs 10s ×1 upton68

What hardware would most likely disprove

  1. The work mode. Symptom: slots written and acknowledged, never acted on.
  2. bmsDesignCap being kWh. If it is Ah or Wh, soc_max is wrong by orders of magnitude — and every plan with it.

Two defects the tests caught before commit

  • The login/refresh response carries the token as the envelope's bare data, so the debug trace leaked it. redact() now masks data on those two paths.
  • _post promised never to raise but let a bare OSError escape. Broadened to match alphaess.py.

Security note

Login sends the customer's account password in the request body (over TLS) — the same shape as Sunsynk's password_legacy path.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NMBtMJ5YeGi9yKRofHxhbR

Registers each Hanchu DTU on the user's Hanchu app account as a HanchuCloud
Predbat inverter: monitoring sensors, daily energy counters, and a control path
covering the timed charge and discharge slot tables, both power limits, the
charge target and the device-wide discharge floor.

PROVENANCE. Hanchu publish no API specification. Every endpoint, field and
parameter range was learned by READING three MIT-licensed Home Assistant
integrations (upton68/hanchu-ess-ha, guoxiatech/hanchu-ess-ha,
Blustery7752/hanchu-ess). No code, structure or comment from them is reproduced.
hanchu_const.py annotates each item as OBSERVED in one repo, CORROBORATED across
two or more, or INFERRED - a deduction, not a reading. The control keys and the
device type are corroborated across all three; the work mode that honours the
timed slots is the one significant inference, and hanchu_work_mode_control is
its escape hatch.

Design points:
- Writes are BATCHED into one iotSet per cycle. The reference integration stages
  changes and flushes them in a single call to avoid rate limiting, and Predbat
  touches up to nineteen settings per cycle.
- Token refreshed proactively at ~25 days and re-obtained by logging in again on
  a 401, which this API answers as HTTP 200 carrying code 401.
- The SoC keys accept only 50-100 (charge) and 5-45 (discharge floor), narrower
  than Predbat's range. Values are clamped and the limitation is logged once per
  device and key rather than sent out of range and silently ignored.
- A slot is only ever zeroed if this component previously wrote a real window
  into it, so a user's own manually configured slots are never cleared.
- Every write respects switch.predbat_set_read_only, including the component's
  own time-aware periodic re-apply.

The encrypted station and BMS endpoints are deliberately not called: they need a
vendor AES key hard-coded in the reference integration and carry only per-pack
detail Predbat does not plan with.

templates/hanchu.yaml is the native path. The existing templates/hanchu_cloud.yaml
(HACS integration plus a Home Assistant bridge script) remains as the
alternative and is untouched.

Tests: 67 cases across five files, all passing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NMBtMJ5YeGi9yKRofHxhbR
@upton68

upton68 commented Sep 29, 2026

Copy link
Copy Markdown
Contributor

Thanks for putting this together. I maintain upton68/hanchu-ess-ha and run Predbat against a real Hanchu iESS system, so I can confirm a few of the points listed as inferred:

Work mode 3 ("User-defined") is the mode that honours the timed slots. That matches what I see on my hardware.
Disabling a slot: I disable a slot by setting both its start and end time back to 0:00. Also note that User-defined mode has three timed slots, so the component may want to handle all three rather than just the first.
bmsDesignCap is in kWh. I use it as soc_max in Predbat without any conversion.
Power ratings: the maximum power values returned by the API match the ratings shown in the Hanchu app, and they match the inverter_limit and battery rate values I use in Predbat. So deriving them from the API's power ceilings looks sound on my system.

Provenance: upton68/hanchu-ess-ha is a fork of guoxiatech/hanchu-ess-ha, which is the official Hanchu integration (Hanchu is the trading name of Guoxiatech). So the shared login and request layer comes from the vendor's own code, not from my repo being copied. It isn't two weak sources agreeing, it's one source of truth, and the plaintext-password login at identify/auth/token is the vendor's own flow. I believe Blustery7752's cloud version was reverse-engineered separately, so it's a genuinely independent source. The official version was read-only; the control and write side was added in my fork.

Separately, my own earlier reverse-engineering of the Hanchu cloud API also found AES-encrypted request bodies, so the platform/* family Blustery uses looks real to me, and may be the app's own API, as opposed to the app/ha/* endpoints the official HA integration uses.

Two things from running this in production that may help with the write-pacing and timeout guesses:

Concurrent control calls can fail. I saw the cloud return error 300004 when a charge start was sent right after a discharge stop, or the other way round, as separate control calls with no guaranteed ordering. Serialising all Predbat charge/discharge start and stop calls through a single queued path fixed it. Since then I've had 20+ transitions with no timeouts or 300004 errors. So it's worth making sure the component never has two control writes in flight at once, on top of any pacing.
Response times vary a lot. In my testing, cloud response times ranged from about 7s to 45s. Before I raised the client timeout to 15s per attempt, almost every action needed at least two attempts, so the 15s ×3 retry in the reference is reasonable.

These are from my own system only, so it's worth checking against other models if anyone has them.

…ware feedback

Feedback on #5305 from a maintainer running Predbat on a real Hanchu iESS:
- the cloud refuses overlapping control calls (error 300004), so every
  settings write now goes through one asyncio lock; the equality and pacing
  checks run inside it so a waiting caller does not resend the same payload
- User-defined mode runs all three charge and three discharge slots, and
  00:00-00:00 disables a slot, so unused slots are now written as disabled
  rather than left alone - a leftover app slot would otherwise run against
  the plan
- provenance upgraded to HARDWARE for work mode 3, slot disabling,
  bmsDesignCap in kWh; GUOXIA recorded as Hanchu's own integration

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NMBtMJ5YeGi9yKRofHxhbR
@mgazza

mgazza commented Sep 29, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks, this is exactly what we couldn't get from the code alone. Pushed da2f091:

  • Every control write now goes through a single lock, so two can never be in flight (the 300004 case). Charge and discharge were already batched into one iotSet, but the schedule button and the reconcile loop could each start one.
  • Since User-defined runs all three slot pairs and 00:00–00:00 disables a slot, Predbat now writes every slot it doesn't use as disabled, so a leftover app slot can't run against the plan.
  • Provenance updated: work mode 3, slot disabling and bmsDesignCap in kWh are marked as hardware-confirmed (on your system), and GUOXIA is recorded as Hanchu's own integration.

The 15s × 3 retry stays as is. If you're able to try this branch on your system at some point, that would be the best test it could get.

@upton68

upton68 commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Thanks for the quick turnaround, those changes cover what I raised. A few more points from running this in practice:

Mid-window end-time changes. Predbat can lengthen or shorten a window's end time while it's running. From the code, the rebuilt payload should pick that up. On my system, rewriting a whole slot mid-window works fine, and setting start and end to 00:00 stops a window immediately, both in the integration and in the Hanchu app. I haven't tried stopping by moving the end time to now or earlier. Is a mid-window end-time change covered by a test?

Failed writes. The risky case is a rejected shorten: the inverter keeps the longer end time and carries on past the plan (and a rejected lengthen ends the window early). The code retries, but as far as I can see it only logs a warning. In my integration a write is retried three times, and if it still fails I get a phone notification so I can step in manually. It might be worth surfacing a persistent write failure somewhere more visible, if Predbat has a suitable mechanism. I'm also not sure whether the 60s pacing hold could delay a retry after a failure.

On the power ranges: the 5000 defaults are overridden per device by the menu, which is right, since Hanchu units come in 3.68, 5, 6 and 10 kW. I've only confirmed the 5 kW case, where the menu maximum matches the app. It would be worth logging a warning when the menu doesn't return the power bounds and the 5000 default is in use. It might also be worth asking someone with a 3.68, 6 or 10 kW unit to confirm the menu values, and adding an inverter_limit override alongside the battery_rate_max one.

I'd be glad to help test this. I run Predbat in a couple of setups already, so I'd try the branch as a separate read-only instance to check login, sensors and limits first, and report back before anything writes to the inverter.

… sooner, inverter_limit override

Second round of hardware feedback on #5305:
- three rejected settings writes in a row now raise a Predbat notification
  and flag a non-fatal error (once per streak), and recovery is logged; a
  rejected shorten otherwise runs the battery past the plan unnoticed
- a failed write is retried after 30s instead of the full pacing interval,
  so the next cycle retries it; successful writes are paced as before
- warn once when a device's menu answers without power bounds, since the
  5 kW default is wrong for 3.68, 6 and 10 kW units
- new hanchu_inverter_limit override alongside hanchu_battery_rate_max
- tests for mid-window shorten, lengthen, end-now and cancel

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NMBtMJ5YeGi9yKRofHxhbR
@mgazza

mgazza commented Sep 30, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks again, all three taken on board in f208e3f:

  • Mid-window changes: there are now tests for shortening, lengthening, moving the end to "now" and cancelling a running window. Each produces one write carrying the new slot times; cancelling writes 00:00–00:00.
  • Failed writes: three rejected writes in a row now raise a Predbat notification (once per streak) and flag a non-fatal error, and recovery is logged. You were right about pacing: a failed write was held for the full interval. It's now retried after 30s, so the next cycle picks it up. Successful writes are still paced normally.
  • Power ranges: a one-off warning is logged when a unit's menu answers without power bounds and the 5000 W default is in use, and there's a new hanchu_inverter_limit override alongside hanchu_battery_rate_max.

A read-only instance first sounds ideal. Set hanchu_control_enable: false (or keep switch.predbat_set_read_only on) and it won't write anything. Login, sensors and the derived limits are the things I'd most like checked. If anyone you know has a 3.68, 6 or 10 kW unit, the menu power bounds would be very useful too.

@upton68

upton68 commented Sep 30, 2026

Copy link
Copy Markdown
Contributor

Thanks for the quick turnaround in f208e3f. The mid-window tests, the failure notification and the 30s retry all sound right. I'll also ask around for owners of 3.68, 6 and 10 kW units to try the read-only setup and report back on the menu power bounds. I'd still like to try it read-only myself as a separate instance, checking login, sensors and derived limits, and I'll report back before anything writes to the inverter.

The next section slipped my mind previously so I am bringing it your attention now.

AC- vs DC-coupled systems. I run AC-coupled, and I've worked with a DC-coupled contributor on my own integration, so I've seen the two differ. The clearest case was a change that merged status and statistics polling into a single getDeviceStatus call. That was fine on DC but broke the daily energy sensors on AC-coupled. So it's good that this component keeps getDeviceStatus and getDeviceStatistics separate, and I'd keep it that way.
On PV, I can answer the question in hanchu_const.py. On my AC-coupled system, pvTtPwr does not include the AC-coupled PV generation. That is only reported in bypMeterTotalPower. The daily pv figure in getDeviceStatistics, however, does cover both, so on an AC-coupled system the live pv_power will under-report compared with pv_today, unless bypMeterTotalPower is combined with it.
There is also a signal for coupling type: bypMeterSwitch is 1 on AC-coupled systems and 0 on DC-coupled ones. The code currently says there's no such signal, so this could be used to detect it, and to decide whether to add bypMeterTotalPower into the PV figure Predbat sees.
This is from my system only, so it's worth checking on other units.

@upton68

upton68 commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Tested f208e3f read-only on a real 5 kW AC-coupled system: login, discovery, iotGet, status and statistics all work, and batSoc, bmsDesignCap and the power settings match the app. My existing cloud integration and Predbat instance stayed online throughout, so a second login doesn't seem to disturb them.

I found two problems that together mean the per-device ranges from /gateway/app/ha/menu are never used:

The menu returns code 200, but _post only accepts a response with success true, so it logs "was unsuccessful (code 200)" and refresh_config discards the data. The official Hanchu integration (guoxiatech/hanchu-ess-ha, async_get_menu in api.py) accepts the menu on code == 200 alone.
Even when accepted, _post returns only the inner data, but _parse_menu_ranges looks for a data key inside what it receives, so it finds no ranges.

With both changes (below) the parse found all five ranges: charge and discharge power 0 to 5000, charge SoC 50 to 100, discharge floor 5 to 45 and grid charge SoC 20 to 100. With only the first change it found none.

-            if payload.get("success"):
+            if payload.get("success") or (endpoint_key == "menu" and code == 200):
                 return True, payload.get("data")

-                ranges = self._parse_menu_ranges(menu)
+                ranges = self._parse_menu_ranges({"data": menu})

On my 5 kW unit the menu values equal the built-in defaults, so nothing changes for me. For 3.68, 6 and 10 kW models the defaults would be wrong, so I'd expect this to matter for them. I also noticed _warn_if_default_power runs only inside if ok:, so it can't warn when the menu call itself fails. I haven't looked at the unit tests, but if they pass the whole response to the parser, that may be why this wasn't caught.

@upton68

upton68 commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

Regarding my comment on 30th September re AC vs DC Systems.

Daytime check on the AC-coupled system (f208e3f, read-only): pvTtPwr 0.0 W, bypMeterTotalPower 686.8 W, bypMeterSwitch 1. Load was 278.8 W, the battery was charging at 408.0 W and the grid was 0 W, so load plus battery equals bypMeterTotalPower exactly. The AC-coupled solar is only in bypMeterTotalPower, so pv_power (from pvTtPwr) reads zero while the system generates. The daily pv in getDeviceStatistics was 2.19 kWh, so it does count the AC-coupled generation.

For comparison, on a DC system (a contributor's getDeviceStatus output) bypMeterSwitch is 0, bypMeterTotalPower is 0 and pvTtPwr carries the solar (load 630 W = battery 615 W + 15 W PV, grid 0). So bypMeterSwitch distinguishes the two, and adding bypMeterTotalPower to pvTtPwr gives the right live PV on both, since it adds nothing on DC. The one case I can't speak to is a system with both DC strings and AC-coupled PV, where I don't know whether pvTtPwr already includes the AC part.

springfall2008 and others added 3 commits October 19, 2026 20:15
`python3 hanchu.py`, in the shape of the Fox, Solis and AlphaESS
harnesses, so someone with a Hanchu system can prove or disprove what
hanchu_const.py marks INFERRED:

- default: read-only. One run() with control off, then per device the
  work mode, the slot table, each range and whether it came from the
  menu or the built-in default, the AC/DC coupling fields, the derived
  ratings, and the args automatic_config would set.
- --write-test: writes a charge window three hours away, reads it back
  until it matches (cloud round trips take 7-45s), then restores.
  Proves iotSet without moving any energy.
- --live-window charge|export|freeze-charge|freeze-export: runs that
  Predbat state now through the component's own build_write_payload(),
  samples the battery until the window ends, prints PASS/FAIL/
  INCONCLUSIVE, then restores. Windows start on Predbat's 5-minute grid.
  A freeze is only judged when it could show something (house demand,
  or a solar surplus).
- --restore SNAPSHOT: writes back the snapshot every test saves to disk
  before its first write.

Every write asks first (--yes to skip), Ctrl-C restores, and the
password can come from HANCHU_PASSWORD, --config apps.yaml or a prompt.
The helpers that decide what is written, restored and judged are tested
in tests/test_hanchu_cli.py; the drivers that talk to the cloud are not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… reserve at 45%

Three defects, the first two found by upton68 on a real 5 kW
AC-coupled system (batpred#5305):

- Menu ranges were never applied. The menu answers code 200 with no
  success flag, so _post discarded it, and _parse_menu_ranges looked
  for a nested data key that _post had already unwrapped. Every unit
  ran on the 5 kW defaults, which are wrong for the 3.68, 6 and 10 kW
  models. The menu is now judged on code 200, as the official
  integration does; every other endpoint still needs success: true.
  A failed menu call now also gives the one-off default-power warning.
- On an AC-coupled system the solar is only in bypMeterTotalPower and
  pvTtPwr reads 0, so pv_power read zero while pv_today climbed. Where
  bypMeterSwitch is 1 the bypass meter is now added into pv_power. A
  system with both DC strings and AC-coupled PV has not been seen yet.
- The discharge floor only accepts 5-45%, but inverter_reserve_max was
  left at 100. Hold Charging at 80% therefore wrote a reserve of 81,
  clamped to 45, and the battery drained to 45% before re-charging from
  the grid. automatic_config now sets it to the lowest floor ceiling
  across devices, as one value because inverter.py reads it without an
  index. It is also in the discovery record, and the template's hint
  of 98 now says 45.

The harness's live-window judge now reads solar from pv_power alone,
which already carries AC-coupled solar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fline status, one HTTP session

From comparing the component with upton68/hanchu-ess-ha v2.0.6 and the
vendor's own settings card:

- The charge target now goes in DTU_AC_CHG_SOC_LMT, "Grid to Battery
  Charge Maximum" (20-100). CHG_BAT_SOC_LMT, "Maximum Charge SOC", is
  never written: by its name it is the battery's overall ceiling, so
  writing a 70% charge target into it would also stop solar charging at
  70% for the rest of the day. Inferred from upton68's entity names, so
  the harness gains --target, to watch whether the inverter stops a
  charge window at the target by itself.
- The menu tells us which time-slot format a device uses. Where it edits
  TCT_START_1, the work mode written with a window comes from that
  item's listener, rather than a hard-coded 3, when it names any. Where
  it shows only the other format (array-valued 82/83 periods and
  FLAG_ENABLE_CYCLE), the device would accept TCT_ writes and ignore
  them, so it is monitored but not controlled, and the status says why.
  A menu that shows neither still allows control.
- An overlapping charge and export window are never both written - the
  vendor card refuses to submit that. The running window is kept,
  otherwise the one starting sooner, and the drop is logged.
- devStatus is read. An offline datalogger is logged once per outage,
  published as a status sensor, and keeps the component from reporting
  itself healthy, since its readings are stale.
- One HTTP session is reused for every call and closed on shutdown.
  upton68 saw far fewer timeouts after the same change.
- The generator fields (hasDg, dgPAcTotal, dgEp) are read and
  published, and the harness prints them beside load, grid and battery,
  so a live test can show their sign and whether they are already
  counted elsewhere. Nothing Predbat plans with uses them yet.

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

upton68 commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

Correction to my earlier comment. I said the 5000 W defaults would be wrong for 3.68, 6 and 10 kW models. Having checked Hanchu's user manuals, the picture is:
Single phase, 3.0 to 6.0 kW (HESS-HY-S-3.0-6.0K, section 11.2): max charge and discharge power is 5000 W for all of the 3.0K, 3.68K, 4.0K, 5.0K and 6.0K. The 5000 W defaults are correct for battery power on these. The AC rating varies (section 11.3: 3000/3680/4000/5000/6000 W), so inverter_limit derived from the discharge ceiling (5000 W) overstates the 3, 3.68 and 4 kW units and understates the 6 kW one. Owners of these models should set hanchu_inverter_limit.
Single phase, 7/8/10 kW (HESS-HY-S-7K-10K, section 10.1): max charge and discharge power is 7000/8000/10000 W, equal to the AC rating.
Three phase:
HESS-HY-T-05/06/08/10/12K (section 10.1): 5000/6000/8000/10000/12000 W charge and discharge.
HESS-HY-T-4K-M to 12K-M and T-10K-M4 to 20K-M4 (section 11.1): 4 to 20 kW charge power. Discharge is given only as a current (25 A or 40 A).
Commercial CESS-HY-25K to 50K and 40K-P/50K-P: 25 to 50 kW charge power, 100 A.
In all of these the battery power ceiling equals the rated AC output, so inverter_limit derived from it is correct.
What this means for the defaults: the 5000 W default is right only for the single-phase 3–6 kW units and for 5 kW three-phase. Every larger model, and the T-4K-M, needs the menu values to be read correctly, which depends on the menu-parsing fix from my test report.
Model naming overlaps: S-6.0K (5000 W), T-06K (6000 W) and T-6K-M (6 kW) are all "6 kW", and 10 kW and 12 kW exist in several ranges. Any per-model matching needs the full model string.
(There are also 48 V off-grid-style HESS-HY-S-6K-S/12K-S units. Their datasheets give only charge currents, so I've left them out.)

springfall2008 and others added 2 commits October 20, 2026 20:09
…eiling to the held value

Live-tested with the harness on a HESS-HY-S-6.0K (nameplate 6000 W AC,
18.8 kWh, DC-coupled):

- A zero discharge rate on its own does NOT hold the battery. Outside a
  slot, User-defined mode behaves as self-use, and the battery kept
  feeding a 1 kW house load for ten minutes with DSCHG_PWR_LMT at 0.
  The menu shows both power limits only in User-defined mode, beside the
  slots, so they set the slot rates rather than capping the battery.
- An open charge slot at zero charge power DOES hold it. The battery
  went from feeding the house at 2.8 kW to 0 W within a minute, and the
  house ran from the grid. Unlike the reserve, which tops out at 45%,
  this works at any SoC.

So every hold Predbat asks for - a discharge rate of 0 with no export
running (freeze charge, hold charging, car and iBoost holds) - is now
written as a charge slot at zero power. It goes in slot 3, which
Predbat's own window never uses, runs from the current half hour to an
hour on (rewritten at most every 30 minutes, and lapsing within the
hour if Predbat stops renewing it), and is cut short of any slot it
would overlap. Inside a running charge window whose target is met, the
window itself carries the hold at zero charge power. The reserve is
never written above the current SoC: the slot does the holding, and a
reserve above the SoC could make the inverter charge from the grid.

Freeze-export - a charge rate of 0 with no window running - becomes the
mirror image, a discharge slot at zero power. NOT yet tested on
hardware; to be checked with surplus solar.

The menu's 0-5000 W power range is one entry shared by 16 models from
2 kW to 10 kW, so it is not a rating: the 6.0K holds DSCHG_PWR_LMT 6000.
A power ceiling is now raised to any higher value the device holds,
which gives this unit's nameplate figures (inverter_limit 6000,
battery_rate_max 5000), and each power control advertises its own
direction's maximum.

Also confirmed on hardware: battery and grid signs, kW/W units that
switch between calls, the per-key responseSemMap on iotSet, and the
menu's slot format and work mode.

Harness: the account e-mail is masked in traces; --dump-menu saves the
full menu and the report lists its energy items and raw status fields;
--target judges whether a charge or export stops at the target by
itself; the new hold test; freeze-charge raises the reserve to SoC + 1
as execute.py does, and is judged a failure if it charges from the grid.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…zero-power discharge slot

Predbat's freeze export sends surplus solar to the grid while the
battery still covers the house. A discharge slot at zero power was
live-tested on a HESS-HY-S-6.0K and stopped the battery covering the
house (its discharge fell from 895 W to about 60 W and the house ran
from the grid), so it is the wrong tool and is removed.

Freeze export is back to Predbat's own signal, a zero charge rate,
passed through with no slot, so the inverter stays in self-use and the
battery keeps covering the house. Whether the zero charge rate stops the
battery absorbing surplus solar outside a slot is still to be tested in
sunshine. If it does not, "Maximum Charge SOC" held at the current SoC
would, from 50% up.

The hold (a charge slot at zero power) is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@springfall2008
springfall2008 marked this pull request as ready for review October 20, 2026 19:28
@github-actions github-actions Bot added the BOT_REVIEW Trigger an autotriage label Oct 20, 2026
springfall2008 and others added 2 commits October 20, 2026 20:29
…ma requires (#4925)

The float_keys_float_defaults test on main rejects a float-declared
apps.yaml key read with an int default, which truncates a configured
fraction.

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

Copy link
Copy Markdown
Owner

I'm going to merge this, so please raise tickets for new bugs/issues

@springfall2008
springfall2008 merged commit b743b50 into main Oct 20, 2026
2 checks passed
@springfall2008
springfall2008 deleted the feat/hanchu-component branch October 20, 2026 19:39
@springfall2008

Copy link
Copy Markdown
Owner

Automated review failed to complete for this PR. Reason: Claude ran out of turns before finishing (--max-turns) (exit status 1). Failed at: triage_daemon.py:1962 in review_pr() (daemon 063549db). Log: pr-5305-review.log in the bot's logs directory. Not retrying automatically; remove BOT_FAILED and re-add BOT_REVIEW to try again.

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

Labels

BOT_FAILED An automated triage/PR attempt failed; see the comment for details

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants