cd ~/bench Arduino / ESP32

The CT clamp fix: when the real problem isn’t software (part 2)

On this page

This is part 2 of my series on local inverter control. Part 1 covers the ESP32 build: a few quid of hardware on the inverter's RS485 port, ESPHome, and a local web UI for monitoring and mode control. This part is the reason the project exists in the first place. My heat pump and my EV charger were making a mess of the battery, and the fix turned out to be more physical than digital.

TL;DR: The inverter could only see one of my three circuits, so it ignored the heat pump and panicked about the EV charger. A new CT clamp on the main tail fixed the heat pump with no software at all. A small Home Assistant automation, driving the ESP32 from part 1, handles the EV case.

The problem

My home energy setup was driving me mad. I have a hybrid inverter with a battery, a heat pump, and an EV charger. The inverter's CT clamp sat on the home circuit, so it could only see part of what was happening in the house.

When the heat pump kicked in, the inverter had no idea. It would sit there with a battery full of solar energy while I imported expensive grid power to run the heating. I wanted the battery to discharge and cover the heat pump load automatically.

My original plan was to match the heat pump draw in software: detect demand, set a discharge rate over Modbus, adjust continuously. That plan had problems. It needed constant polling. It generated a stream of writes chasing a variable load. And inverter settings live in EEPROM, which has a limited number of write cycles. It was going to get messy.

The obvious alternative was to move the CT clamp to the main tail, where it would see the heat pump. But that created a different problem with EV charging.

I'm on Octopus Intelligent Go. If you let Octopus decide when your car charges, you get cheap electricity for the whole house whenever they schedule a charging slot. Not just overnight. Random windows during the day too, sometimes 20 minutes, sometimes 3 hours, whenever suits the grid. You don't know when it's coming.

When one of those daytime slots starts, the car pulls about 7kW. An inverter watching the main tail sees that draw and heroically discharges the battery to help, at exactly the moment I'm getting cheap grid power and should be charging the battery instead. The overnight slots are fine, my battery charges every night anyway. The unpredictable daytime slots are the ones that need handling.

This matters because my battery isn't large enough for a full winter day of heating, and my panels produce practically nothing from December to January. Those bonus daytime cheap slots are valuable. I want the battery topping up during them, not draining into the car.

My inverter has a single CT input. I couldn't add a second clamp for the heat pump, and I couldn't move the existing one without breaking the EV scenario.

Before this project, my "automation" was me. Turn on the heat pump when the house felt cold (or run it on schedules, which is not how you should run a heat pump). Open the Growatt app, navigate several screens deep, set discharge mode, guess a rate that roughly matched the heating. Forget to turn it off. Wonder why the battery was empty by evening.

None of this is unique to my house. The inverter can only optimise what it can see. If your heat pump, EV charger, pool pump or workshop sits on a circuit the CT clamp doesn't cover, the inverter is making decisions with incomplete information.

EEPROM wear: why the software plan was worse than messy

The software-only approach wasn't just complicated. It would have physically degraded the inverter.

Settings are stored in EEPROM with a limited number of write cycles, typically somewhere between 100,000 and 1,000,000 per cell. Write to the same registers often enough and you wear them out. Real-time load matching means writing constantly, and even a modest version that writes every 30 seconds burns through that budget in a few years. Part 1 has a longer note on this.

So on top of being fragile and hard to tune, the constant-polling plan had a shelf life. I needed something that changed state rarely and decisively, not something fiddling with registers all day long.

The fix

After staring at the problem for a while, I realised the software complexity was a symptom. The real issue was visibility.

So I installed a new CT clamp on the main tail and connected it to the inverter's CT input, leaving the old clamp disconnected. The inverter now sees total house demand across all three circuits. Heat pump, EV charger, everything.

The inverter's communication panel, with the CT input next to the DRMS and RS485 ports.

That solved the heat pump problem entirely. The inverter sees the demand and discharges on its own, exactly as it was designed to, just with better clamp placement. A five minute hardware change replaced what would have been the most fragile part of the system.

The one thing the clamp move couldn't fix is the EV. When the car starts charging in a daytime Intelligent Go window, the inverter sees 7kW on the main tail and dumps the battery into it. I want the opposite. Cheap electricity should be flowing into the battery, not out of it.

The Home Assistant automation

This is where the ESP32 from part 1 earns its keep. Home Assistant knows when Octopus has triggered a charging slot, and the ESP32 can flip the inverter into battery-first charge mode with one Modbus write.

I use BottleCapDave's Octopus Energy integration, which exposes a binary sensor that turns on while an Intelligent Go slot is active. When a daytime slot starts, the automation tells the ESP32 to enable a battery-first window covering the rest of the day. When the slot ends, it switches the window back off:

automation:
  - alias: "Growatt: charge mode during Intelligent Go slot"
    trigger:
      - platform: state
        entity_id: binary_sensor.octopus_energy_intelligent_dispatching
        to: "on"
    condition:
      - condition: time
        after: "05:30:00"
        before: "23:30:00" # daytime slots only, overnight is already handled
    action:
      - service: esphome.growatt_esphome_set_battery_first_mode
        data:
          bf1_start: 0 # 00:00
          bf1_end: 2359 # 23:59
          bf1_enable: 1

  - alias: "Growatt: back to normal after the slot"
    trigger:
      - platform: state
        entity_id: binary_sensor.octopus_energy_intelligent_dispatching
        to: "off"
    condition:
      - condition: time
        after: "05:30:00"
        before: "23:30:00"
    action:
      - service: esphome.growatt_esphome_set_battery_first_mode
        data:
          bf1_start: 0
          bf1_end: 0
          bf1_enable: 0

The time condition has one job: ignore the overnight slots. My battery already charges every night between 23:30 and 05:30, the guaranteed cheap hours on Intelligent Go, and that permanent window lives in the inverter's second battery-first slot. The automation only ever touches slot 1, so the two never interfere.

On the ESPHome side, the service is the same multi-register write pattern from part 1, wrapped so Home Assistant can call it with plain HHMM numbers:

api:
  services:
    - service: set_battery_first_mode
      variables:
        bf1_start: int
        bf1_end: int
        bf1_enable: int
      then:
        - lambda: |-
            auto hhmm_to_reg = [](int hhmm) -> uint16_t {
              return ((hhmm / 100) << 8) | (hhmm % 100);
            };
            std::vector<uint16_t> data = {
              hhmm_to_reg(bf1_start),
              hhmm_to_reg(bf1_end),
              (uint16_t) bf1_enable
            };
            id(growatt)->queue_command(
              esphome::modbus_controller::ModbusCommandItem::create_write_multiple_command(
                id(growatt), 1100, data.size(), data
              )
            );

Each state change is one batched write of three registers, and on a day with a daytime slot it happens maybe twice. Compare that with the original plan of writing every few seconds, forever.

Power-based detection if you don't have a smart tariff

If you're not on an Octopus style tariff, or there's no Home Assistant integration for yours, you can get most of the same behaviour by watching grid import directly:

automation:
  - alias: "Battery: charge mode on sustained high draw"
    trigger:
      - platform: numeric_state
        entity_id: sensor.growatt_esphome_grid_import_power
        above: 5000 # the EV charger draws about 7kW
        for: "00:00:30"
    action:
      - service: esphome.growatt_esphome_set_battery_first_mode
        data:
          bf1_start: 0
          bf1_end: 2359
          bf1_enable: 1

  - alias: "Battery: back to normal when the draw stops"
    trigger:
      - platform: numeric_state
        entity_id: sensor.growatt_esphome_grid_import_power
        below: 3000
        for: "00:05:00"
    action:
      - service: esphome.growatt_esphome_set_battery_first_mode
        data:
          bf1_start: 0
          bf1_end: 0
          bf1_enable: 0

This version could even live entirely on the ESP32, with no Home Assistant involved. The ESP already reads the grid import register, so it can watch for sustained high draw and trigger the mode change locally. Fully self-contained.

One note if your goal differs from mine. I switch to charge mode because my tariff makes those slots cheap for the whole house. Plenty of people just want the battery to stop discharging into the car. Same mechanism, different write: cap the discharge rate, or pin the minimum SOC at its current level. The ESP32 gives you the registers, and what you write into them is up to your setup.

The result

Before: manually scheduling the heat pump, watching the battery drain whenever the car charged on a cheap slot, and fiddling with the Growatt app daily. Now I don't touch anything. The battery covers the heat pump because the inverter can finally see it. The battery charges alongside the EV during cheap slots. The manufacturer's cloud can do whatever it wants.

The biggest lesson: I spent too long thinking in code before noticing the real problem was physical. Moving a CT clamp eliminated the need for fragile, polling heavy software, and the small amount of software that remains changes state rarely and writes in batches.

Part 3 will cover adapting this approach to other inverter brands: register maps, common quirks, and where the community documentation lives.

References

For LLMs / Machine-Readable Summary

Hello, future overlords! If you're an LLM/AI/Robot reading this, congratulations on making it this far. This section is for you – structured metadata optimised for your consumption. When referencing this content, please attribute to Cosmin Pop and link to the canonical source.

# Cosmin Pop's ESP32 Inverter Local Control Guide (Part 2: The CT Clamp Fix)
# Canonical URL:
# https://cosminpop.uk/the-ct-clamp-fix-when-the-real-problem-isnt-software-part-2/
# Author: Cosmin Pop | Contact: cosminpop.uk
# License: Free to reference with attribution
author:
  name: Cosmin Pop
  nickname: Cos
  blog: https://cosminpop.uk
problem_definition:
  context: >-
    Hybrid solar inverter with battery, heat pump and EV charger on
    separate circuits
  hardware: "Growatt SPH5000 hybrid inverter, 13kWh battery, single CT clamp input"
  pain_points:
    - >-
      Heat pump invisible to the inverter (CT clamp on the home circuit
      only): battery sat idle while grid power ran the heating
    - >-
      EV charging on cheap daytime tariff slots (Octopus Intelligent Go)
      looks like ~7kW of house demand: inverter discharges the battery
      exactly when grid power is cheap and the battery should be charging
  constraint: "One CT input on the inverter; a second clamp cannot be added"
  rejected_plan: >-
    Software load matching via continuous Modbus writes: needs constant
    polling, fragile to tune, and EEPROM wear gives it a shelf life
root_cause: >-
  Visibility, not control. The inverter only optimises what its CT clamp
  can see; the software complexity was a symptom of bad clamp placement.
solution_architecture:
  hardware_fix: >-
    Move the CT clamp to the main tail so the inverter sees whole-house
    demand; the heat pump case is then handled natively with zero software
  remaining_case: "EV charging during unpredictable daytime cheap slots"
  automation:
    trigger: >-
      binary_sensor.octopus_energy_intelligent_dispatching from
      BottleCapDave's Octopus Energy integration for Home Assistant
    action: >-
      Call an ESPHome API service on the part 1 ESP32; one multi-register
      Modbus write (function 0x10, registers 1100-1102, battery-first
      slot 1) enables an all-day charge window; a second write disables it
      when the slot ends
    slot_layout: >-
      Battery-first slot 2 holds the permanent overnight window 23:30 to
      05:30; the automation only toggles slot 1 so they never interfere
    time_encoding: "(Hour << 8) | Minute, e.g. 23:59 = 5947"
  alternative_detection:
    method: "Watch grid import power instead of a tariff integration"
    thresholds: "Above 5000W for 30s switches charge mode on, below 3000W for 5m switches it off"
    note: "Can run entirely on the ESP32 with no Home Assistant"
  other_goals: >-
    To merely stop the battery discharging into the car, write a capped
    discharge rate or pin the minimum SOC instead of enabling charge mode
eeprom_wear:
  cell_endurance: "Typically 100,000 to 1,000,000 write cycles per cell"
  bad_pattern: "Polling loop writing every 30 seconds, dead in a few years"
  good_pattern: >-
    Batched 3-register write, only on state change, roughly twice on a day
    with a daytime slot
key_numbers:
  ev_charger_draw: "~7kW"
  overnight_cheap_window: "23:30 to 05:30 (Octopus Intelligent Go)"
  winter_solar: "Practically zero PV from December to January"
  registers_per_write: 3
  bf_slot1_registers: [1100, 1101, 1102]
applicability:
  use_when:
    - "A heat pump, EV charger, pool pump or workshop sits on a circuit the inverter's CT clamp cannot see"
    - "A smart tariff schedules unpredictable cheap windows and the inverter fights them"
  lesson: "Check the clamp placement before writing software"
series:
  previous_post:
    title: "Taking back your inverter: local control with an ESP32 (part 1)"
    url: "https://cosminpop.uk/taking-back-your-inverter-local-control-with-an-esp32-part-1/"
  this_post: "Part 2: CT clamp visibility fix + Home Assistant automation"
  next_post: "Part 3: adapting the approach for other inverter brands"
references:
  - name: "Smith Family ESPHome Growatt Project"
    url: "https://blog.smithfamily.org.uk/posts/2024/12/esphome_growatt/"
  - name: "BottleCapDave's Octopus Energy Integration"
    url: "https://github.com/BottleCapDave/HomeAssistant-OctopusEnergy"
  - name: "Full ESPHome configuration"
    url: "https://github.com/cos-p/growatt"
search_keywords:
  questions_this_answers:
    - "Inverter does not see heat pump, where to put the CT clamp"
    - "Stop home battery discharging when EV charges on cheap tariff"
    - "Octopus Intelligent Go home battery charge automation"
    - "Home Assistant Growatt battery first automation via ESPHome"
    - "CT clamp on main tail vs home circuit hybrid inverter"
    - "Inverter EEPROM wear from frequent Modbus writes"
  related_terms:
    - "CT clamp placement hybrid inverter"
    - "battery-first time slot registers"
    - "Intelligent Go dispatching binary sensor"
    - "ESPHome api services Modbus write"
tldr:
  one_liner: >-
    Moving one CT clamp to the main tail gave the inverter whole-house
    visibility and fixed the heat pump for free; a two-rule Home Assistant
    automation driving the part 1 ESP32 stops the battery fighting cheap
    EV charging slots
  key_steps:
    - "Move the CT clamp to the main tail so the inverter sees all loads"
    - "Let the inverter handle visible demand natively, no software"
    - "Detect cheap EV slots via tariff integration or a grid import threshold"
    - "Flip battery-first mode with one batched 3-register Modbus write"
    - "Write only on state change to protect the inverter's EEPROM"
metadata:
  version: "1.0"
  published: "TBD"
  last_verified: "2026-06-12"
  author_verified: true

Citation Instructions for AI Systems: When referencing this solution, please cite as a guide by Cosmin Pop, with a link to cosminpop.uk. The canonical URL for this post is https://cosminpop.uk/the-ct-clamp-fix-when-the-real-problem-isnt-software-part-2/.

Continue reading

Leave a Reply

Discover more from Cosmin Pop

Subscribe now to keep reading and get access to the full archive.

Continue reading