Metadata-Version: 2.4
Name: juham-terracharger
Version: 0.0.4
Summary: ABB Terra EV charger plugin for Juham
Author-email: J Meskanen <juham.api@gmail.com>
Maintainer-email: "J. Meskanen" <juham.api@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://gitlab.com/juham/juham/juham-terracharger
Project-URL: Bug Reports, https://gitlab.com/juham/juham/juham-terracharger
Project-URL: Funding, https://meskanen.com
Project-URL: Say Thanks!, http://meskanen.com
Project-URL: Source, https://gitlab.com/juham/juham/juham-terracharger
Keywords: object-oriented,plugin,framework,terracharger,home automation
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Topic :: Software Development
Classifier: Programming Language :: Python :: 3.8
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE.rst
Requires-Dist: juham-automation>=0.2.29
Requires-Dist: pymodbus>=3.15.0
Requires-Dist: importlib-metadata
Provides-Extra: dev
Requires-Dist: check-manifest; extra == "dev"
Dynamic: license-file

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``** (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/>`_.
