Skip to content

SolaxModbus + Growatt MID: TOU begin/end writes silently dropped, slot stuck on stale window #181

Description

@jdungen

Symptom

On a Growatt MID 15KTL3-XH with the wills106/homeassistant-solax-modbus integration, the BESS-planned battery mode never actually takes effect. The schedule shows e.g. EXPORT_ARBITRAGE → grid_first, the BESS log shows Modbus: TOU segment 1 = grid_first (00:00-23:59), but the inverter behaves as Load First — battery discharges at ~200W following house load rather than the planned 15kW push to grid.

Root cause

set_tou_segment_via_entities() in ha_api_controller.py writes the four parts of TOU slot 1 to these entities:

Slot field Entity used Outcome
tou_time_1_enabled select.growatt_inverter_time_1_active ✅ written, _read sensor updates
tou_time_1_mode select.growatt_inverter_time_1_mode ✅ written, _read sensor updates
tou_time_1_begin select.growatt_inverter_time_1_begin silently dropped
tou_time_1_end select.growatt_inverter_time_1_end silently dropped

The select.*_time_N_begin/end entities exist in HA's registry but their state is permanently unavailable with restored: true. The SolaX Modbus integration drops select.select_option calls against these entities — service_call returns success at HA's service layer, but the value never reaches the Modbus register. The subsequent button.press time_1_update then commits whatever begin/end was already in the inverter (in my case 18:45-23:10, presumably a stale value from an earlier session or factory default), with the new mode.

Net effect: slot 1 is active only inside that stale window, and outside that window the inverter falls back to default Load First — regardless of what BESS planned.

Reproduction

  1. Growatt MID 15KTL3-XH + wills106/homeassistant-solax-modbus integration
  2. Let BESS write TOU slot 1 normally (any cycle)
  3. Inspect:
    • select.growatt_inverter_time_1_begin.stateunavailable, restored: true
    • sensor.growatt_inverter_time_1_begin_read.state → some old time (NOT what BESS just wrote)
    • Battery behavior mismatches the planned mode outside the stale window

Reloading the integration via homeassistant.reload_config_entry does NOT clear the unavailable state.

The working alternative

The SolaX Modbus integration also exposes a parallel set of entities under the time domain:

time.growatt_time_1_begin   state: 18:45:00   (works — valid state, writes accepted)
time.growatt_time_1_end     state: 23:10:00   (works — valid state, writes accepted)

Calling time.set_value on these entities:

  1. Updates the HA state immediately
  2. Stages the value for the next button.press time_1_update
  3. The _read sensor reflects the new value after the button press
  4. The inverter actually behaves accordingly

I verified this live: after manually writing time.growatt_time_1_begin = 00:00 and time.growatt_time_1_end = 23:59 then pressing the update button, time_1_begin_read flipped to 00:00, time_1_end_read to 23:59, and battery discharge jumped from 184W → 14876W (Grid First export activated).

Proposed fix (defensive — needs design discussion)

I don't yet know whether:

  1. The select.* route is broken on every Growatt model with this integration, or only MID/MOD
  2. The time.* entities exist on Growatt MIN / SPH platforms

Naively switching select.select_optiontime.set_value for begin/end could break MIN/SPH users if their integration version doesn't expose the time.* entities yet.

Suggestion for a safe PR:

  • At controller init, probe whether time.<prefix>_begin exists and is not unavailable
  • If yes → write via time.set_value; if no → fall back to select.select_option
  • Either path: still call the button.press *_update to commit

Would appreciate input from other users on whether they see this on MIN or SPH Growatt inverters before scoping the PR.

Workaround for affected users right now

In HA Developer Tools → Services:

  1. time.set_value on time.growatt_time_1_begin with time: 00:00:00
  2. time.set_value on time.growatt_time_1_end with time: 23:59:00
  3. button.press on button.growatt_inverter_time_1_update

After this, BESS continues to write mode/active correctly via select.* (those work), the broken begin/end writes are no-ops, and the update-button commits the existing 00:00-23:59 window — self-healing from there.

Environment

  • BESS Manager 9.6.2-jvdd.6 (fork of 9.6.2 — relevant TOU code unchanged from upstream)
  • Home Assistant Core (recent)
  • wills106/homeassistant-solax-modbus integration (current release)
  • Growatt MID 15KTL3-XH, ~30 kWh battery

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions