Terra Charger for Juham™
Note: Very early Alpha release (untested and under active development). Do not use for controlling chargers yet.
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(aJuhamThread) 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(aMasterPieceThread) 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 everypoll_intervalseconds via the framework'supdate()/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_hourshours ending at or beforecharge_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 thejuham_core/masterpiecepackages 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_port502- Modbus TCP port (502 is the ABB default).
-
reg_serial_number0x4000- Register: encoded charger serial number (read).
-
reg_firmware_version0x4004- Register: firmware version (read).
-
reg_error_code0x4008- Register: error code,
0= no error (read).
-
reg_current_limit_setpoint0x4100- Register: requested current limit, Amps (write).
-
reg_start_stop0x4105- Register: start/stop charging session (write).
-
reg_current_limit_readback0x4109- Register: applied current limit (read) -- unconfirmed, see the register map section below.
-
max_house_current32.0- Total current (A) your installation must never exceed across all loads combined -- typically your main fuse rating.
-
max_charger_current16.0- The charger's own maximum current capability (A). The controller never requests more than this even if house headroom allows it.
-
min_charge_current6.0- Below this current (A), the charger pauses rather than continuing to charge at a reduced rate.
-
voltage230.0- Nominal phase voltage, used to convert between Watts (as reported on the power topic) and Amps (as required by the charger).
-
phases3- Number of phases used for the power/current conversion (1 or 3). Assumes a roughly balanced load across phases.
-
required_charge_hours4.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_hour7- 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_threshold0.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.5means "cheaper than the recent median".
-
poll_interval2.0- Seconds between worker
update()ticks -- Modbus health polling, schedule recomputation, current-limit application, and status publishing all happen on this cadence.
-
spot_history_retention172800(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 areal_totalfield 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 withTimestampandPriceWithTaxfields.
Published
-
<site>/terra_charger_status-- current controller state, published on every workerupdate()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_windowis the currently computed cheapest-block window (start/end epoch seconds), ornullif 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:
- 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.
- The deadline is computed as the next occurrence of
charge_by_hourintimezone, rolling over to tomorrow if already past today. - 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_hoursis 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_readbackis 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. timezonemust be set correctly for your location.charge_by_houris 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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file juham_terracharger-0.0.4.tar.gz.
File metadata
- Download URL: juham_terracharger-0.0.4.tar.gz
- Upload date:
- Size: 22.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.12.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ab375b337cfae5d4c8fbfa7d486adc0d9c551e4688247696e199ea17f5a0b4da
|
|
| MD5 |
5f46e5b2ed418bbc944db827645d6701
|
|
| BLAKE2b-256 |
7062780a9ffcf32b4695f89935da3abfc095ed32f07ebd6531b041269e5e599b
|
File details
Details for the file juham_terracharger-0.0.4-py3-none-any.whl.
File metadata
- Download URL: juham_terracharger-0.0.4-py3-none-any.whl
- Upload date:
- Size: 12.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.12.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8e064780527fd55a16b230311d2b1273bb09c6f64cb50eae5989287984b46893
|
|
| MD5 |
35e2983ed2add2b07cdba8bccdfff26a
|
|
| BLAKE2b-256 |
28d9ae7dbb5258f8a11af30b6ca29a3ced97219909235e282f62d92286052122
|