Skip to main content

Terra Charger for Juham™

terracharger is a Juham <https://gitlab.com/juham/>_ component that performs dynamic load control for an ABB Terra AC EV charger over Modbus TCP. It watches the household's live power consumption and a day-ahead spot electricity price feed, and continuously adjusts the charger's current limit so that:

  • your home's total current draw never exceeds a configured maximum (e.g. your main fuse rating, 32A), and
  • charging happens during the cheapest contiguous block of hours before the car needs to be ready, rather than just reacting to whatever the price happens to be at the moment.

It is designed to sit alongside EnergyCostCalculator and other Juham components, communicating purely over MQTT, with a single dedicated Modbus TCP connection to the charger.

Architecture

terra_charger follows the standard Juham main/worker split:

  • TerraCharger (a JuhamThread) is a thin facade that owns MQTT subscriptions, configuration, and serialization. It never talks to the charger directly -- it just forwards the two pieces of live state the worker needs (house_power, spots) as plain attribute writes.
  • TerraChargerThread (a MasterPieceThread) owns the Modbus TCP connection and does all blocking device I/O: reading the error register, computing the day-ahead schedule, applying the current limit, and publishing status -- once every poll_interval seconds via the framework's update()/update_interval() hooks.

Splitting it this way means a slow or unresponsive charger connection never blocks MQTT message handling.

Features

  • Dynamic current limiting. On every update() tick, computes remaining current headroom under a configured house-wide limit from the latest household power reading, and writes it to the charger as its current setpoint.
  • Day-ahead cheapest-window scheduling. Given known spot prices, finds the cheapest contiguous block of required_charge_hours hours ending at or before charge_by_hour (local time), and only allows full-headroom charging inside that window. The settlement slot duration is inferred from the price data itself, so this works whether your feed publishes hourly or 15-minute prices.
  • Opportunistic fallback. If a full schedule can't be computed yet (e.g. tomorrow's prices haven't been published, or there's too little price history), falls back to a simple percentile check -- "is the price right now cheaper than the recent median" -- so the car still charges opportunistically rather than sitting idle.
  • Safe minimum-current handling. The Terra AC pauses charging rather than smoothly ramping down below min_charge_current (6A by default) -- this component respects that by stopping the session instead of writing an unsupported sub-minimum value.
  • Self-healing Modbus I/O. A read/write exception drops the client reference, so the next update() tick automatically reconnects rather than needing a bespoke retry path.
  • Background health polling. Every update() tick also reads the charger's error-code register and republishes status, independent of how often power/price MQTT messages arrive.

Requirements

  • A Modbus TCP-capable ABB Terra AC charger (the LCD/networked variant -- some CE non-display Terra AC models do not support Modbus TCP/IP at all).
  • Modbus TCP enabled on the charger via the ABB TerraConfig app (Bluetooth setup → Connectivity → enable Modbus TCP server), with a static IP address assigned to the charger.
  • Python 3.10+, pymodbus (3.x sync client API), pytz, and the juham_core / masterpiece packages already used by the rest of your Juham setup.

.. note:: The charger only accepts one active Modbus TCP session at a time. If you also have the TerraConfig app, an EVCC instance, or any other tool connected over Modbus, disconnect it before running this component. A phone app (e.g. MyToyota) that talks to the car/charger over Wi-Fi for its own purposes is a separate channel and does not conflict with this.

Installation

.. code-block:: bash

pip install juham-terracharger

Then enable the component the same way you enable other Juham automation components in your application's configuration.

.. code-block:: python

from terra_charger import TerraCharger

charger = TerraCharger(name="terra_charger") charger.modbus_host = "192.168.1.42" charger.max_house_current = 32.0 charger.max_charger_current = 16.0 charger.phases = 3 charger.required_charge_hours = 4.0 charger.charge_by_hour = 7 charger.timezone = "Europe/Helsinki"

The following configuration parameters are supported:

.. list-table:: :header-rows: 1 :widths: 25 12 63

    • Attribute
    • Default
    • Description
    • modbus_host
    • "192.168.1.50"
    • IP address of the Terra AC charger. Set a static IP for it in your router or via TerraConfig.
    • modbus_port
    • 502
    • Modbus TCP port (502 is the ABB default).
    • reg_serial_number
    • 0x4000
    • Register: encoded charger serial number (read).
    • reg_firmware_version
    • 0x4004
    • Register: firmware version (read).
    • reg_error_code
    • 0x4008
    • Register: error code, 0 = no error (read).
    • reg_current_limit_setpoint
    • 0x4100
    • Register: requested current limit, Amps (write).
    • reg_start_stop
    • 0x4105
    • Register: start/stop charging session (write).
    • reg_current_limit_readback
    • 0x4109
    • Register: applied current limit (read) -- unconfirmed, see the register map section below.
    • max_house_current
    • 32.0
    • Total current (A) your installation must never exceed across all loads combined -- typically your main fuse rating.
    • max_charger_current
    • 16.0
    • The charger's own maximum current capability (A). The controller never requests more than this even if house headroom allows it.
    • min_charge_current
    • 6.0
    • Below this current (A), the charger pauses rather than continuing to charge at a reduced rate.
    • voltage
    • 230.0
    • Nominal phase voltage, used to convert between Watts (as reported on the power topic) and Amps (as required by the charger).
    • phases
    • 3
    • Number of phases used for the power/current conversion (1 or 3). Assumes a roughly balanced load across phases.
    • required_charge_hours
    • 4.0
    • Hours of charging needed to fill the battery from wherever it currently is. A fixed estimate, since this component has no telemetry link to the car's actual state of charge -- set it to roughly what a typical top-up needs.
    • charge_by_hour
    • 7
    • Local hour (0-23, in timezone) by which charging must be complete. Rolls over to tomorrow if "now" is already past this hour.
    • timezone
    • "Europe/Helsinki"
    • IANA timezone name used to interpret charge_by_hour. Explicit and configured, rather than relying on the host machine's system timezone -- important if this ever runs on a server/container defaulted to UTC.
    • price_percentile_threshold
    • 0.5
    • Fallback favorability threshold, used only when no day-ahead schedule can be computed yet: fraction (0..1) of recently seen spot prices that must be above the current price for "now" to count as favorable. 0.5 means "cheaper than the recent median".
    • poll_interval
    • 2.0
    • Seconds between worker update() ticks -- Modbus health polling, schedule recomputation, current-limit application, and status publishing all happen on this cadence.
    • spot_history_retention
    • 172800 (48h)
    • How far back to retain spot price history, in seconds. Only needs to cover recent past for the percentile fallback, since the scheduler itself only looks forward.

MQTT topics

Subscribed

  • <site>/powerconsumption -- total household power, JSON payload with a real_total field in Watts. Includes whatever the EV itself is already drawing, since it's measured at the main meter.
  • <site>/spot -- spot electricity prices, a list of records each with Timestamp and PriceWithTax fields.

Published

  • <site>/terra_charger_status -- current controller state, published on every worker update() tick:

    .. code-block:: json

    { "name": "terra_charger", "charging_active": true, "requested_current": 11.0, "house_power": 4200.0, "error_code": 0, "scheduled_window": [1735707600.0, 1735718400.0], "ts": 1735732800.0 }

    scheduled_window is the currently computed cheapest-block window (start/end epoch seconds), or null if no feasible schedule has been found yet (see below).

Day-ahead scheduling

TerraChargerThread.compute_charging_schedule() finds the cheapest contiguous block of required_charge_hours hours, ending at or before the charge_by_hour deadline, using currently known spot prices:

  1. The settlement slot duration is inferred from the data itself (the median gap between consecutive known price timestamps), so this works whether your price feed is hourly or 15-minute.
  2. The deadline is computed as the next occurrence of charge_by_hour in timezone, rolling over to tomorrow if already past today.
  3. A sliding-window sum over all known future slots before the deadline finds the minimum-cost window of the required length.

If there isn't enough known future price data yet to cover a full window (e.g. tomorrow's prices aren't published, or there's too little history), compute_charging_schedule() returns None rather than raising, and the controller falls back to the percentile-based price_is_favorable() check so the car still charges opportunistically instead of sitting idle. The schedule is recomputed on every update() tick, which is also what rolls the deadline over to the next day once it has passed.

ABB Terra AC Modbus register map

.. list-table:: :header-rows: 1 :widths: 15 15 70

    • Register
    • Access
    • Purpose
    • 0x4000
    • read
    • Encoded charger serial number.
    • 0x4004
    • read
    • Firmware version.
    • 0x4008
    • read
    • Error code (0 = no error).
    • 0x4100
    • write
    • Requested current limit, in Amps.
    • 0x4105
    • write
    • Start/stop charging session (1 = start, 0 = stop).

All of the above are plain configurable attributes (see the Configuration table), so a firmware-specific correction doesn't require a code change.

.. warning:: These addresses come from ABB's public "Terra AC Charger Modbus Communication" reference and are known to vary across firmware revisions. Verify every address against the datasheet shipped with your unit's firmware before relying on it. In particular, the readback register for the applied current limit ( reg_current_limit_readback) is not yet confirmed -- ABB's documentation is ambiguous between 0x4109 and 0x4024 across firmware revisions. Until it's confirmed and wired in, the controller estimates the EV's own draw from what it last requested rather than reading it back directly.

Known limitations

  • required_charge_hours is a fixed estimate, not derived from actual battery state. This component has no telemetry link to the car, so it can't size the charging window from real state of charge. If you ever get access to that (e.g. an API, or the charger's own session-energy register), that's the natural next upgrade.
  • EV draw is estimated, not measured, until reg_current_limit_readback is confirmed and wired in (see above). Until then, headroom calculation infers non-EV household load by subtracting the EV's own last-requested current from total house power, which will drift from reality if the charger's internal load-management algorithm doesn't honor the requested value exactly.
  • No hard floor below min_charge_current. The Terra AC pauses rather than trickles below 6A, so fine-grained control below that threshold isn't possible -- the controller stops the session instead.
  • timezone must be set correctly for your location. charge_by_hour is only meaningful relative to the configured IANA timezone name, not wherever the host machine's system clock happens to be set.
  • pymodbus version sensitivity. The Modbus calls target pymodbus 3.x's synchronous client API; adjust if you're running 2.x.

Testing

.. code-block:: bash

python -m unittest discover tests

See tests/terracharger/test_terracharger.py for unit tests covering both classes: TerraChargerThread (Modbus I/O, schedule computation, load control, status publishing) and TerraCharger (MQTT plumbing, forwarding to the worker, and config serialization).

License

See the license file in the root of the juham project <https://gitlab.com/juham/>_.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

juham_terracharger-0.0.3.tar.gz (26.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

juham_terracharger-0.0.3-py3-none-any.whl (15.9 kB view details)

Uploaded Python 3

File details

Details for the file juham_terracharger-0.0.3.tar.gz.

File metadata

  • Download URL: juham_terracharger-0.0.3.tar.gz
  • Upload date:
  • Size: 26.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.12.14

File hashes

Hashes for juham_terracharger-0.0.3.tar.gz
Algorithm Hash digest
SHA256 e4a70898941452db39a3f96bc45dad27b8f40df983f2d7863b0bc7765618d10a
MD5 7e86e323c5ffe3bb62d1cbbe41061d58
BLAKE2b-256 bcccd30e81a2ddb3a29741571718ca13647610cd8bc3eaef3173792bc41075bf

See more details on using hashes here.

File details

Details for the file juham_terracharger-0.0.3-py3-none-any.whl.

File metadata

File hashes

Hashes for juham_terracharger-0.0.3-py3-none-any.whl
Algorithm Hash digest
SHA256 c9be7d0047e9181075ead848ea503293cd2d626d3e2b328852ee3ffccac0881c
MD5 22f4f753b1dadace0bd1219801514e03
BLAKE2b-256 b1ce3d85b407ca51edbf3e6a9ae907eb5671cca3ae5112a14211dc4e8ff6a9d7

See more details on using hashes here.

Release history Release notifications | RSS feed

0.0.4

2 files

This release

0.0.3 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page