# Modbus TCP Documentation

> This document describes the Modbus TCP interface for the eCharge EVCS firmware: the full register map, transport and session rules, timing and failsafe behavior, and worked integration examples. It applies to the local EVCS at Unit ID `1`.

> **Document version:** 2.1.0 (register map `MapVersion 2.1.0`). **Firmware baseline:** the release carrying `MapVersion 2.1.0` - read `MapVersion` (`40001`) on the station to confirm. **Last updated:** 2026-08-20. See **Document history** at the end for what changed from the previous (`2.0.1`) revision.

> **Upgrading from a** `MapVersion 2.0.0` integration. No register moved and nothing was removed, so an existing client keeps working. Four behaviours changed and one block is new: the charge-current bounds are now taken from the station's own window, starting with a zero current target is reported as `0x03` instead of `0x04`, `EVCS_State` reports CHARGING for sessions the EMS did not start, over-temperature clears by itself, and the station's actual charge mode / EcoPlus / pause state are readable at IR `30601-30603`. Each is described in place below and summarised in **Document history**.

---

## Overview

The firmware exposes a standards-based Modbus TCP interface over Ethernet for remote monitoring and control of charging operations. External systems (e.g. EMS/SCADA) read telemetry and write configuration or setpoints using a contiguous, non-overlapping register map: **Input Registers** for read-only data and **Holding Registers** for read/write data.

### Scope

This specification applies to an **AC, unidirectional** electric vehicle charging station (EVCS) with optional **1↔3-phase switching**. **DC charging** and **bidirectional (V2G)** operation are **not supported**.

**Register coverage**

* **Identification**: serial number, model, manufacturer, firmware version.
* **Configuration**: current limits, eco/efficiency modes, and the station's own current settings read back separately from what the EMS asked for.
* **Operational state**: station state, connector state, authorization status, fault/error codes, daemon health.
* **EVSE and CP parameters**: availability, per-connector current limits, CP status, contactor/plug status, phase-switching status.
* **Metering telemetry**: per-phase and total voltage, current, power, energy, temperature.
* **Mains / battery telemetry**: grid and storage data sourced from the station.
* **Control points**: current target, start/stop charging, failsafe timeout and fallback current, phase selection.

---

## Protocol - Transport and Session

### Connection

* **Server role**: the EVCS implements a **Modbus TCP server** on **TCP port 502**.
* **Clients**: external systems (EMS/SCADA) act as **clients/masters** and initiate all requests; the EVCS responds as server/slave.
* **IP configuration**: DHCP (default) or static IP.
* **Unit ID**: default `0x01`. The Unit ID is configurable (valid range `1-247`). This document describes the EVCS served at Unit ID `1`. The same gateway may also serve proxied downstream slave devices and a Fronius SMEM emulator at other Unit IDs - those have their own documentation.

### Connection and onboarding

Before your integration can talk to the charger, be aware of the connection-level controls (details under **Security considerations**):

* **IP allow-list (optional).** If your installer has enabled it, only listed source IP addresses may connect. A connection from an unlisted IP is **closed immediately with no Modbus reply** - this looks exactly like the device being offline (a TCP reset). If you cannot connect at all, confirm your client IP is on the allow-list.
* **Connection limits.** Up to **32** concurrent connections total and **4** per source IP. Excess connection attempts are dropped at accept (again, no Modbus reply).
* **Rate limit.** Up to **50 requests/second** per source IP (burst **100**). Requests over the limit receive exception `0x06` (Server Device Busy) on an open connection - back off and retry.
* **Startup.** While the daemon is still starting up, every request is answered with exception `0x06`. Poll **DaemonStatus** (`30050`) and gate automation on readiness.

### Message format (MBAP + PDU)

Each Modbus TCP ADU consists of:

* **MBAP header**

    * **Transaction Identifier**
    * **Protocol Identifier** (always `0x0000` for Modbus)
    * **Length** (number of following bytes)
    * **Unit Identifier**
    
* **PDU (Protocol Data Unit)**

    * **Function code**
    * **Data payload**
    

### Transport characteristics

* TCP/IP provides reliable, in-order delivery and flow control.
* Maximum MBAP length is **254 bytes** (per the Modbus TCP specification).

### Supported Function Codes

| Function | Code (hex/dec) | Purpose | Notes |
| --- | --- | --- | --- |
| Read Holding Registers  | `0x03 / 3`  | Read one or more holding registers (configuration and setpoints).  | Reads up to **125** contiguous registers per request.  |
| Read Input Registers  | `0x04 / 4`  | Read one or more input registers (read-only telemetry).  | Data is returned from a **consistent snapshot**.  |
| Write Single Register  | `0x06 / 6`  | Write a single holding register.  | Out-of-range values are rejected with exception `0x03`.  |
| Write Multiple Registers  | `0x10 / 16`  | Write multiple contiguous holding registers **atomically**.  | Accepts **1-123** registers; misaligned or partial-word frames return exception `0x03`.  |

**Notes**

* Unsupported function codes return exception `0x01` (Illegal Function). The optional discovery functions Report Slave ID (`0x11`) and Read Device Identification (`0x2B/0x0E`) are **not supported** and return `0x01`.
* "Consistent snapshot" ensures multi-register reads of the same value (e.g. a 32-bit float) are not mixed across updates.
* "Atomic write" ensures either all registers in the request are applied together, or none are.

### Exception Codes

If the EVCS cannot fulfill a request, it returns a Modbus **exception response** (function code OR'ed with `0x80`) with a one-byte exception code:

| Code (hex/dec) | Name | When it occurs |
| --- | --- | --- |
| `0x01 / 1`  | Illegal Function  | The requested function code is not supported (e.g. discovery FCs `0x11`/`0x2B`).  |
| `0x02 / 2`  | Illegal Data Address  | The address (or range) is outside the implemented map, or a write targets a valid-but-not-writable holding address.  |
| `0x03 / 3`  | Illegal Data Value  | The value is out of range, the target is read-only, or a multi-register write is malformed (misaligned/wrong length).  |
| `0x04 / 4`  | Slave Device Failure  | An internal error prevented execution - the command reached the device but could not be delivered to the charging controller. It does **not** indicate a bad value; a rejected value is always `0x03`.  |
| `0x06 / 6`  | Server Device Busy  | You are being rate-limited (retry shortly), or the daemon is still completing startup (retry, and check DaemonStatus).  |
| `0x0A / 10`  | Gateway Path Unavailable  | Only when addressing a gateway-forwarded **slave** device: the remote slave is unreachable. The local EVCS (Unit ID `1`) never returns this.  |
| `0x0B / 11`  | Gateway Target Device Failed to Respond  | The addressed **Unit ID** is not served by this gateway - check your Unit ID.  |

> **Note on connection refusals.** Being blocked by the IP allow-list or a connection limit is **not** a Modbus exception - the TCP connection is closed with no PDU. If your client reports "connection refused/reset" rather than a Modbus exception, suspect the allow-list or a connection limit, not a device fault.

### Session behaviour and timing

* **Response format**: all responses follow the Modbus TCP ADU structure (MBAP header + PDU) with correct Modbus byte/word order.
* **Latency target**: p95 &lt; **200 ms** under nominal LAN conditions.
* **Consistency**: multi-register reads are returned from a coherent snapshot; multi-register writes are atomic.
* **Client polling**: poll telemetry at an interval of **≤ 5 s**; refresh control setpoints (or the heartbeat) more often than the configured failsafe timeout (see **Timing rules**).
* **Validation**: out-of-range values are **rejected** with exception `0x03`; the register keeps its previous value (no clamping).
* **Timeouts**: a connection is closed after **5 minutes** idle; each request/response operation has a **15 s** deadline.

---

## Data representation and endianness

### Endianness

* **Word order**: multi-register values use **big-endian word order** (high word at the lower register address).
* **Byte order**: each 16-bit register uses **big-endian byte order** (MSB first).

### Data types

* `float32` (IEEE-754): power, voltage, current, energy, CP duty cycle, mains/battery values.
* `uint16` (u16): enumerations, bit-fields, small numeric quantities.
* `uint32` (u32): counters (Uptime) and **Temperature** (integer degrees Celsius - see note).
* **Bit-fields**: unless stated otherwise, bit `0` is the LSB of the register.

> **Temperature is a** `u32` integer, not a float. `Temperature` (`30025`) is a big-endian `u32` in whole degrees Celsius (sub-zero values read as `0`). Do not decode it as IEEE-754.

### Strings

* ASCII, **two characters per 16-bit register** (MSB = first character).
* Unused bytes are `0x00`. Strings are fixed-length and not null-terminated beyond padding; clients must trim trailing zeros.

### Scaling

* Registers carry **final engineering-unit values** (V, A, W, Hz, kWh, degrees C) at **scale 1.0**. The daemon has already converted the firmware's internal milli/centi units, so no additional scaling is required on the client side.

### Validation

* Each writable register defines valid bounds; values outside these bounds are **rejected** with exception `0x03` and the register is **unchanged**. There is no clamp-to-nearest behavior, so re-reading a register after a rejected write shows the previous value.

### Concurrency and atomicity

* **Single-writer semantics**: register commits are serialized internally and applied one at a time; writes never fail for concurrency reasons. Side effects run after the commit, so if two clients write the same register at nearly the same moment the later commit wins - an exception returned to the earlier client does not guarantee the register kept its previous value. Read the register back if you need to be certain of the resulting state.
* **Atomic multi-register writes**: a multi-register (FC16) write is applied all-or-nothing. Misaligned or partial-word frames are rejected with exception `0x03`.
* **Read/write interaction**: reads do not block writes. A read may be delayed briefly to return a coherent snapshot while an in-progress write completes.

### Security considerations

Modbus TCP itself provides no confidentiality, integrity, or authentication - the transport is **plaintext** (no TLS, no client authentication). Deploy the charger on an isolated/trusted network or behind a secure gateway. In addition, the EVCS enforces the following transport-level controls:

| Control | Behavior |
| --- | --- |
| IP allow-list  | Optional. If enabled, connections from unlisted source IPs are closed at accept (no Modbus reply). Enabling it requires at least one valid address range.  |
| Per-IP rate limit  | 50 requests/second per source IP, burst 100. Over-limit requests return exception `0x06`.  |
| Connection limits  | Maximum 32 concurrent connections total and 4 per source IP. Excess attempts are dropped at accept.  |
| Idle / IO timeouts  | Idle connections are closed after 5 minutes; each operation has a 15 s deadline (protects against stalled clients).  |
| Startup gate  | Requests are answered with exception `0x06` until the daemon finishes startup.  |

> The IP allow-list, Unit ID, role, and related settings are configured by your eCharge installer/operator (via the charger's web configuration). Ask them to add your EMS source IP to the allow-list before integrating.

---

## Addressing conventions

* **PDU addressing**: Modbus register addresses in the PDU are **zero-based**.
* **Human-readable notation**: documentation shows `30001+` (Input) and `40001+` (Holding). Address = `Offset + 30001` (Input) or `Offset + 40001` (Holding).
* **Width**: all addresses refer to 16-bit registers; multi-word values occupy consecutive registers with the high word at the lower address.
* **Reserved areas**: reserved entries are omitted from the register tables unless their behavior matters (`40077`, `40108`, `30051-30059`, `30604-30610`); they must not be accessed. Reads of reserved areas return `0` and writes may return exception `0x02`.

| Class | Zero-based offset | Human-readable |
| --- | --- | --- |
| Input  | `0`  | `30001`  |
| Input  | `15`  | `30016`  |
| Holding  | `0`  | `40001`  |
| Holding  | `42`  | `40043`  |

---

## Device identity and versioning

The EVCS exposes a stable identity interface - serial number, model identifier, manufacturer, and firmware version - to support registration, compatibility checks, and remote management. Identity fields are read-only **Holding** registers (FC03), ASCII strings packed two characters per register.

* **Serial number** - unique per device; constant over the device lifetime.
* **Model identifier** - product family.
* **Manufacturer** - vendor name.
* **Firmware version** - the running software build, as a string and as a packed `u32`.
* **Register-map version** - `MapVersion` indicates the revision of this Modbus map. Read `MapVersion` together with `FirmwareVersion` to verify compatibility; a `MapVersion` major-version change may relocate registers.

> `MapVersion` (`2.1.0`) and `ModelType` (`EV_CHARGING_STATION`) are fixed firmware constants and do not vary per station. A station still reporting `2.0.0` is running an older firmware: the register layout is the same, but the behaviours listed in the upgrade note at the top of this document differ.

**Discovery** - the optional Modbus functions Report Slave ID (`0x11`) and Read Device Identification (`0x2B/0x0E`) are **not yet supported**; both return exception `0x01`.

---

## Register map overview

Each row describes one field. Columns: **Doc Register** (human-readable `3xxxx`/`4xxxx`), **PDU Addr** (zero-based offset), **Name**, **Type**, **Len** (number of 16-bit registers), **Units**, **Access** (`R` = read-only, `RW` = read/write, `W` = write-only - reads of a `W` register are not rejected; they return the last stored value, which carries no meaning), **Persist** (`NV` = non-volatile, `VOL` = volatile), **Notes**.

> All offsets refer to 16-bit registers; multi-word values occupy consecutive registers with big-endian word and byte order (high word at the lower address). Reserved entries must not be accessed; a few are listed below where their behavior matters.

### Register blocks at a glance

| Block | Address range | Function code | Access |
| --- | --- | --- | --- |
| EVCS Identity  | `40001-40077`  | FC03  | R  |
| EVCS Configuration  | `40101-40108`  | FC03 / FC06 / FC16  | R, with RW setpoints `40104-40107`  |
| General State + Daemon health  | `30001-30059`  | FC04  | R  |
| EVSE (offered limits)  | `30101-30115`  | FC04  | R  |
| Charge - CP / Plug / Contactor  | `30201-30229`  | FC04  | R  |
| Phase Switching  | `30301-30306`  | FC04  | R  |
| Actual configuration state  | `30601-30603`  | FC04  | R  |
| RFID  | `30401-30433`  | FC04  | R  |
| Mains / Battery  | `30501-30527`  | FC04  | R  |
| Meter Identity  | `41001-41026`  | FC03  | R  |
| Meter Telemetry  | `31001-31024`  | FC04  | R  |
| Control points  | `40201-40225`  | FC03 / FC06 / FC16  | RW / W  |

### EVCS Identity

| Doc Register | PDU Addr | Name | Type | Len | Units | Access | Persist | Notes |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| `40001`  | `0`  | MapVersion  | ASCII\[16\]  | 8  | --  | R  | NV  | Fixed `2.1.0`; 2 chars/reg; zero-padded  |
| `40009`  | `8`  | Model  | ASCII\[32\]  | 16  | --  | R  | NV  | e.g. `cPH22T22`  |
| `40025`  | `24`  | SerialNumber  | ASCII\[16\]  | 8  | --  | R  | NV  | e.g. `102178283`  |
| `40033`  | `32`  | Manufacturer  | ASCII\[32\]  | 16  | --  | R  | NV  | e.g. `eCharge Hardy Barth`  |
| `40049`  | `48`  | FirmwareVersionStr  | ASCII\[16\]  | 8  | --  | R  | NV  | e.g. `2.3.75`  |
| `40057`  | `56`  | FirmwareVersionU32  | u32  | 2  | --  | R  | NV  | e.g. `0x0203004B` for firmware `2.3.75` (`2 -> 0x02`, `3 -> 0x03`, `75 -> 0x004B`; plain binary, not BCD); word order High-Low  |
| `40065`  | `64`  | ModelType  | ASCII\[24\]  | 12  | --  | R  | NV  | Fixed `EV_CHARGING_STATION`  |
| `40077`  | `76`  | _reserved_  | u16  | 1  | --  | R  | NV  | Reserved tail of the identity block; reads `0`  |

### EVCS Configuration

| Doc Register | PDU Addr | Name | Type | Len | Units | Access | Persist | Notes |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| `40101`  | `100`  | FuseRating  | u16  | 1  | A  | R  | NV  | Fuse rating  |
| `40102`  | `101`  | EcoPlusTimeout  | u16  | 1  | s  | R  | NV  | Configured EcoPlus timeout  |
| `40103`  | `102`  | EcoReference  | u16  | 1  | --  | R  | NV  | PV-surplus trigger threshold  |
| `40104`  | `103`  | MinCurrent  | u16  | 1  | A  | RW  | NV  | Minimum charging current; valid `6-32`. Tracks the station: also updated when the limit is changed outside Modbus  |
| `40105`  | `104`  | MaxCurrent  | u16  | 1  | A  | RW  | NV  | Maximum charging current; valid `6-32`. Tracks the station: also updated when the limit is changed outside Modbus  |
| `40106`  | `105`  | EcoPlus  | u16  | 1  | bool  | RW  | NV  | `0 = off`, `1 = on`. EMS setpoint only; the station's actual value is IR `30602`  |
| `40107`  | `106`  | ChargeMode  | u16  | 1  | enum  | RW  | NV  | See **Enums -> Charge Mode** (`0-2`). EMS setpoint only; the station's actual value is IR `30601`  |
| `40108`  | `107`  | _reserved_  | u16  | 1  | --  | R  | NV  | Not writable; an FC06/FC16 write returns exception `0x02`  |

> The read-only config block is `40101-40103`; the writable setpoints are the contiguous block `40104-40107`. An FC06/FC16 write to a read-only config register (`40101-40103`) returns exception `0x03`.

**Setpoints and actual values.** The four writable config registers do not all behave the same way, because a charger can also be changed from its web interface, from the app, or by an RFID card.

* `MinCurrent` (`40104`) and `MaxCurrent` (`40105`) are setpoint and readback at once. They define the charge-current window this device will accept (see **Control points**), so they always state the window actually in force. If the window is changed outside Modbus, the new values appear here within one telemetry cycle (**≤ 2 s**) and the accepted range moves with them. An EMS write takes effect immediately and is never silently reverted.
* `EcoPlus` (`40106`) and `ChargeMode` (`40107`) are EMS setpoints only. The device never overwrites them, so a read-back always returns your own last write. To find out what the station is _actually_ running, read the **Actual configuration state** block (IR `30601-30603`). Comparing the two is how you detect that the station overrode or ignored a setpoint.

> **Writing a new current window.** `MinCurrent` must not exceed `MaxCurrent`. To move the window upward past the current maximum (for example from `6-16` to `20-32`), write both registers in a **single FC16 frame**; the frame is checked as a whole and rejected with `0x03` if it would leave `MinCurrent > MaxCurrent`. Two separate FC06 writes fail on the first one.

### EVCS State

#### General State

| Doc Register | PDU Addr | Name | Type | Len | Units | Access | Persist | Notes |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| `30001`  | `0`  | EVCS_State  | u16  | 1  | enum  | R  | VOL  | See **Enums -> EVCS_State**  |
| `30002`  | `1`  | Connector_State  | u16  | 1  | enum  | R  | VOL  | See **Enums -> Connector_State**  |
| `30003`  | `2`  | StatusWord  | u16  | 1  | bitfield  | R  | VOL  | Reserved (all bits `0`); for daemon health use DaemonStatus (`30050`)  |
| `30004`  | `3`  | ErrorCode  | u16  | 1  | enum  | R  | VOL  | `0 = OK`; see **Enums -> ErrorCode**  |
| `30005`  | `4`  | ChargingInfo  | ASCII\[32\]  | 16  | --  | R  | VOL  | Free-text (e.g. current mode, stop reason)  |
| `30021`  | `20`  | AuthorizationStatus  | u16  | 1  | bool  | R  | VOL  | `0 = no`, `1 = yes`  |
| `30022`  | `21`  | AuthType  | u16  | 1  | enum  | R  | NV  | See **Enums -> Auth Type**  |
| `30023`  | `22`  | Uptime  | u32  | 2  | s  | R  | VOL  | Seconds since boot; word order High-Low  |
| `30025`  | `24`  | Temperature  | u32  | 2  | celsius  | R  | VOL  | Integer degrees C; word order High-Low  |
| `30027`  | `26`  | Failsafe_Active  | u16  | 1  | bool  | R  | VOL  | `1` while the communication-timeout failsafe is active; written internally (no function code can set it)  |
| `30050`  | `49`  | DaemonStatus  | u16  | 1  | bitfield  | R  | VOL  | Daemon health; see **Enums -> DaemonStatus**  |
| `30051..30059`  | `50..58`  | _reserved_  | --  | --  | --  | --  | --  | Reserved for future daemon-health expansion; reads return `0`  |

#### EVSE (offered limits)

| Doc Register | PDU Addr | Name | Type | Len | Units | Access | Persist | Notes |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| `30101`  | `100`  | Availability  | u16  | 1  | bool  | R  | VOL  | `1` when the station reports "operative", else `0`  |
| `30102`  | `101`  | GridCurrentLimit  | u16  | 1  | A  | R  | VOL  | Grid-imposed limit  |
| `30103`  | `102`  | PhaseCount  | u16  | 1  | count  | R  | VOL  | `1` or `3`  |
| `30104`  | `103`  | PhysicalCurrentLimit  | u16  | 1  | A  | R  | VOL  | Hardware limit  |
| `30105`  | `104`  | OfferedCurrentLimit  | u16  | 1  | A  | R  | VOL  | Advertised limit  |
| `30106`  | `105`  | CurrentOfferedTotal  | f32  | 2  | A  | R  | VOL  | Word order High-Low  |
| `30108`  | `107`  | CurrentOfferedL1  | f32  | 2  | A  | R  | VOL  | --  |
| `30110`  | `109`  | CurrentOfferedL2  | f32  | 2  | A  | R  | VOL  | --  |
| `30112`  | `111`  | CurrentOfferedL3  | f32  | 2  | A  | R  | VOL  | --  |
| `30114`  | `113`  | PowerOffered  | f32  | 2  | W  | R  | VOL  | --  |

#### Charge - CP / Plug / Contactor

| Doc Register | PDU Addr | Name | Type | Len | Units | Access | Persist | Notes |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| `30201`  | `200`  | EV_Present  | u16  | 1  | bool  | R  | VOL  | --  |
| `30202`  | `201`  | Charging  | u16  | 1  | bool  | R  | VOL  | --  |
| `30203`  | `202`  | CP_Status  | u16  | 1  | enum  | R  | VOL  | A-F; see **Enums -> CP_Status**  |
| `30204`  | `203`  | CP_PWM_State  | u16  | 1  | bool  | R  | VOL  | `0 = off`, `1 = on`  |
| `30205`  | `204`  | CP_Duty_Cycle  | f32  | 2  | %  | R  | VOL  | Word order High-Low  |
| `30220`  | `219`  | Plug_Lock_Status  | u16  | 1  | enum  | R  | VOL  | `0 = unlock`, `1 = locked`, `2 = unknown`  |
| `30221`  | `220`  | Plug_State_Actual  | u16  | 1  | enum  | R  | VOL  | `0 = unlock`, `1 = lock`  |
| `30222`  | `221`  | Plug_State_Target  | u16  | 1  | enum  | R  | VOL  | `0 = unlock`, `1 = lock`  |
| `30223`  | `222`  | Plug_Error  | u16  | 1  | enum  | R  | VOL  | Firmware-specific plug-lock fault code; `0` = no error, non-zero = fault (not the Table B ErrorCode enum)  |
| `30224`  | `223`  | Contactor_Status  | u16  | 1  | enum  | R  | VOL  | `0 = opened`, `1 = closed`, `2 = unknown`  |
| `30225`  | `224`  | Contactor_State_Actual  | u16  | 1  | enum  | R  | VOL  | `0 = open`, `1 = close`  |
| `30226`  | `225`  | Contactor_State_Target  | u16  | 1  | enum  | R  | VOL  | `0 = open`, `1 = close`  |
| `30227`  | `226`  | Contactor_Error  | u16  | 1  | enum  | R  | VOL  | Firmware-specific contactor fault code; `0` = no error, non-zero = fault (not the Table B ErrorCode enum)  |
| `30228`  | `227`  | PWM_Status  | f32  | 2  | %  | R  | VOL  | Mirror of CP_Duty_Cycle  |

#### Phase Switching

| Doc Register | PDU Addr | Name | Type | Len | Units | Access | Persist | Notes |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| `30301`  | `300`  | PhaseSwitchingEnabled  | u16  | 1  | bool  | R  | NV  | `0/1`  |
| `30302`  | `301`  | PhaseSwitchingStatus  | u16  | 1  | enum  | R  | VOL  | See **Enums -> Phase Switching Status**  |
| `30303`  | `302`  | ActualPhase  | u16  | 1  | count  | R  | VOL  | `1` or `3`  |
| `30304`  | `303`  | TargetPhase  | u16  | 1  | count  | R  | VOL  | `1` or `3`  |
| `30305`  | `304`  | PhaseSwitchDelay  | u16  | 1  | s  | R  | NV  | --  |
| `30306`  | `305`  | PhaseSwitchDuration  | u16  | 1  | s  | R  | NV  | --  |

#### Actual configuration state

Read-only mirrors of settings the **station** owns, whoever changed them - the web interface, the app, an RFID card, or an EMS write. Use them to confirm what the charger is really doing, as opposed to what was last requested through `40106`/`40107`.

| Doc Register | PDU Addr | Name | Type | Len | Units | Access | Persist | Notes |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| `30601`  | `600`  | ChargeMode_Actual  | u16  | 1  | enum  | R  | VOL  | The mode the station is running; see **Enums -> Charge Mode**. Compare against the setpoint at HR `40107`  |
| `30602`  | `601`  | EcoPlus_Actual  | u16  | 1  | bool  | R  | VOL  | `0 = off`, `1 = on`. Compare against the setpoint at HR `40106`  |
| `30603`  | `602`  | PauseCharging_Actual  | u16  | 1  | bool  | R  | VOL  | `1` when charging is paused from the web interface or the app. There is no Modbus register that sets this  |

> `PauseCharging_Actual` has no setpoint counterpart: pause is a local user action, not an EMS command. `Start_Stop_Charging` (`40203`) remains the EMS's own start/stop control and is unaffected by it. If a session will not start and all your setpoints look correct, check this register - a local pause overrides an EMS start.

> Addresses `30604-30610` are reserved and must not be accessed.

#### RFID

| Doc Register | PDU Addr | Name | Type | Len | Units | Access | Persist | Notes |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| `30401`  | `400`  | RFID_Available  | u16  | 1  | bool  | R  | VOL  | --  |
| `30418`  | `417`  | RFID_LastTag  | ASCII\[32\]  | 16  | --  | R  | VOL  | Last seen tag (empty if N/A)  |

### Mains

The Mains/Battery block carries grid and storage data sourced from the station. Fields read `0` until the station publishes them (battery fields stay `0` on chargers without storage).

| Doc Register | PDU Addr | Name | Type | Len | Units | Access | Persist | Notes |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| `30501`  | `500`  | MainsType  | u16  | 1  | enum  | R  | NV  | Connected inverter type  |
| `30502`  | `501`  | MainsAvailable  | u16  | 1  | bool  | R  | VOL  | --  |
| `30503`  | `502`  | MainsPower_Total  | f32  | 2  | W  | R  | VOL  | --  |
| `30505`  | `504`  | MainsCurrent_Total  | f32  | 2  | A  | R  | VOL  | --  |
| `30507`  | `506`  | MainsCurrent_L1  | f32  | 2  | A  | R  | VOL  | --  |
| `30509`  | `508`  | MainsCurrent_L2  | f32  | 2  | A  | R  | VOL  | --  |
| `30511`  | `510`  | MainsCurrent_L3  | f32  | 2  | A  | R  | VOL  | --  |
| `30513`  | `512`  | MainsPower_L1  | f32  | 2  | W  | R  | VOL  | --  |
| `30515`  | `514`  | MainsPower_L2  | f32  | 2  | W  | R  | VOL  | --  |
| `30517`  | `516`  | MainsPower_L3  | f32  | 2  | W  | R  | VOL  | --  |
| `30519`  | `518`  | BatteryAvailable  | u16  | 1  | bool  | R  | VOL  | --  |
| `30520`  | `519`  | BatterySOC  | f32  | 2  | %  | R  | VOL  | --  |
| `30522`  | `521`  | BatteryPower  | f32  | 2  | W  | R  | VOL  | `+` discharge / `-` charge  |
| `30524`  | `523`  | BatterySOC_Start  | f32  | 2  | %  | R  | NV  | --  |
| `30526`  | `525`  | BatterySOC_Stop  | f32  | 2  | %  | R  | NV  | --  |

### Meter Identity

| Doc Register | PDU Addr | Name | Type | Len | Units | Access | Persist | Notes |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| `41001`  | `1000`  | MeterType  | ASCII\[16\]  | 8  | --  | R  | NV  | --  |
| `41009`  | `1008`  | MeterName  | ASCII\[16\]  | 8  | --  | R  | NV  | --  |
| `41017`  | `1016`  | MeterSerial  | ASCII\[16\]  | 8  | --  | R  | NV  | --  |
| `41025`  | `1024`  | MeterPhaseCount  | u16  | 1  | count  | R  | NV  | `1` or `3`  |
| `41026`  | `1025`  | MeterAvailable  | u16  | 1  | bool  | R  | VOL  | --  |

### Meter Telemetry

| Doc Register | PDU Addr | Name | Type | Len | Units | Access | Persist | Notes |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| `31001`  | `1000`  | MeterFrequency  | f32  | 2  | Hz  | R  | VOL  | Word order High-Low  |
| `31003`  | `1002`  | Voltage_L1  | f32  | 2  | V  | R  | VOL  | --  |
| `31005`  | `1004`  | Voltage_L2  | f32  | 2  | V  | R  | VOL  | --  |
| `31007`  | `1006`  | Voltage_L3  | f32  | 2  | V  | R  | VOL  | --  |
| `31009`  | `1008`  | Current_L1  | f32  | 2  | A  | R  | VOL  | --  |
| `31011`  | `1010`  | Current_L2  | f32  | 2  | A  | R  | VOL  | --  |
| `31013`  | `1012`  | Current_L3  | f32  | 2  | A  | R  | VOL  | --  |
| `31015`  | `1014`  | Power_Total  | f32  | 2  | W  | R  | VOL  | --  |
| `31017`  | `1016`  | Power_L1  | f32  | 2  | W  | R  | VOL  | --  |
| `31019`  | `1018`  | Power_L2  | f32  | 2  | W  | R  | VOL  | --  |
| `31021`  | `1020`  | Power_L3  | f32  | 2  | W  | R  | VOL  | --  |
| `31023`  | `1022`  | Energy_Total  | f32  | 2  | kWh  | R  | NV  | --  |

### Control points

| Doc Register | PDU Addr | Name | Type | Len | Units | Access | Persist | Notes |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| `40201`  | `200`  | Set_Charge_Power_Target  | u16  | 1  | W  | RW  | VOL  | Not implemented; writes are accepted and stored but have no effect  |
| `40202`  | `201`  | Set_Charge_Current_Target  | u16  | 1  | A  | RW  | VOL  | Valid `0`, or within the station's window `MinCurrent`-`MaxCurrent` (HR `40104`/`40105`); `0` stops charging  |
| `40203`  | `202`  | Start_Stop_Charging  | u16  | 1  | bool  | RW  | VOL  | `1 = start`, `0 = stop`  |
| `40204`  | `203`  | Failsafe_Timeout  | u16  | 1  | s  | RW  | NV  | Valid `0` or `10-3600`; default `20`; `0` disables failsafe  |
| `40205`  | `204`  | Failsafe_Power_Target  | u16  | 1  | W  | RW  | VOL  | Not implemented; writes are accepted and stored but have no effect  |
| `40206`  | `205`  | Failsafe_Current_Target  | u16  | 1  | A  | RW  | NV  | Valid `0`, or within the station's window `MinCurrent`-`MaxCurrent` (HR `40104`/`40105`); default `0`  |
| `40207`  | `206`  | Reserved_Control_206  | u16  | 1  | --  | RW  | VOL  | Reserved writable no-op; the Failsafe_Active status moved to IR `30027`  |
| `40208`  | `207`  | Heartbeat  | u16  | 1  | --  | W  | VOL  | Write any value periodically to keep failsafe disarmed  |
| `40209`  | `208`  | Target_Phase  | u16  | 1  | count  | RW  | VOL  | Valid `1` or `3`; requires phase switching enabled in device config  |
| `40210`  | `209`  | RFID_AuthorizeReq  | ASCII\[32\]  | 16  | --  | RW  | VOL  | Not implemented; writes to `40210-40225` are accepted and stored but have no effect  |

> **Accepted-as-no-op registers.** `Set_Charge_Power_Target` (`40201`), `Failsafe_Power_Target` (`40205`), `Reserved_Control_206` (`40207`), and `RFID_AuthorizeReq` (`40210-40225`) accept writes (so an FC16 frame can span the whole control block) but have no side effect yet. Do not rely on their stored values affecting charging.

> **Current bounds come from the station, not from this document.** `Set_Charge_Current_Target` and `Failsafe_Current_Target` are validated against the window in `MinCurrent`/`MaxCurrent` (HR `40104`/`40105`) as it stands at the moment of the write. On a station configured for `6-16 A`, a write of `20` is rejected with `0x03` even though `20` is inside the `6-32 A` range the hardware supports. Read `40104`/`40105` at startup, and again whenever a current write is unexpectedly rejected, rather than assuming a fixed range. The absolute `6-32 A` limit applies only to writes of `40104`/`40105` themselves.

---

## Enumerations and error codes

### Table A - EVCS station states

| Code | State | Description |
| --- | --- | --- |
| 0  | NOT_READY  | Not ready to charge (charging disabled or authorisation required).  |
| 1  | READY  | Ready to charge; cable may or may not be connected.  |
| 2  | CHARGING  | Active charging in progress, whoever started the session.  |
| 3  | CHARGING_INTERRUPTED  | Charging interrupted due to EMS request or fault.  |
| 4  | ERROR  | An error is active (`ErrorCode != 0`).  |
| 5  | AUTH_REJECTED  | Reserved; not currently emitted by the firmware.  |
| 6  | RESERVED  | Reserved for future use.  |
| 7  | PHASE_SWITCHING  | Switching between 1-phase and 3-phase operation.  |

> The firmware emits codes `0-4` and `7`. Codes `5` and `6` are reserved and not currently produced - do not build logic that waits for them.

> **CHARGING does not depend on the EMS start command.** `EVCS_State = 2` means the station reports energy actually flowing, no matter how the session began - an EMS start, the web interface, or an RFID card. Two consequences for an existing integration: a session you did not start now shows as CHARGING rather than NOT_READY, and after you write `Start_Stop_Charging = 0` the state leaves CHARGING only once the station has actually stopped (within one telemetry cycle, **≤ 2 s**), not at the instant your write is accepted. To confirm the command itself was accepted, read `Start_Stop_Charging` (`40203`) back. CHARGING_INTERRUPTED and READY still consult the command and are unchanged. See **How the state signals are derived**.

### Table B - Error codes

| Code | Meaning |
| --- | --- |
| 0  | No error.  |
| 1  | Internal hardware fault (self-test failed).  |
| 2  | Over-current or short circuit detected.  |
| 3  | Over-voltage or under-voltage detected.  |
| 4  | Over-temperature detected (electronics; see the note below).  |
| 5  | Communication failure with meter or RFID reader.  |
| 6  | Lock/plug fault.  |
| 7  | Failsafe active.  |
| 8-65535  | Reserved.  |

> **Priority.** When multiple conditions are active at once, the device reports the highest-priority one in this order: `7` Failsafe active > `1` Internal hardware fault > `2` Over-current > `3` Voltage anomaly > `4` Over-temperature > `5` Communication failure > `6` Lock/plug fault. (Priority is the evaluation order, not the numeric code value.)

**Detection conditions**

| Code | Raised when |
| --- | --- |
| 1  | A contactor fault is reported, an emergency shutdown is signalled, or an expected residual-current-device feedback channel goes unavailable.  |
| 2  | The residual current device has tripped, a residual-current fault is reported, or any phase current exceeds **32 A**.  |
| 3  | Any phase voltage is below **195 V** or above **253 V** (nominal 230 V, `-15 %` / `+10 %`). Only evaluated once a non-zero voltage has been read.  |
| 4  | The electronics temperature reaches the over-temperature threshold; see the note below.  |
| 5  | The energy meter is unavailable when one is expected, or the RFID reader is unavailable when one is expected.  |
| 6  | A plug-lock error is reported, or a proximity fault is detected (no cable, invalid resistance).  |

**About over-temperature (code** `4`)

* **It clears by itself.** The code is raised from the **live** electronics temperature and cleared once the reading falls **5 K** back below the threshold, so a station sitting exactly on the limit does not flicker. Recovery needs no restart, reboot, or power cycle: once the station cools, the code returns to `0` within one telemetry cycle. Earlier firmware judged this against a peak-since-startup value that never decreased, so the code stayed raised until the station was restarted - if your integration works around that, the workaround is no longer needed.
* **The threshold varies per station and is not configurable.** Each station reads its own controller's throttling trip point at startup and uses that, so the exact value depends on the temperature grade of the part fitted - commonly **95 degC** on industrial-grade hardware and **85 degC** on commercial-grade, with a **90 degC** fallback if the trip point cannot be read. Do not hard-code a threshold; treat the code itself as the signal.
* **What it measures.** Code `4` reflects the temperature of the charger's control electronics - the same reading exposed at `Temperature` (`30025`). It is not a measurement of the connector, contactor, cable, or enclosure; nothing in this interface reports those.
* **What the charger does about it: nothing.** The station does not derate, throttle, or stop charging on code `4`. It is informational and any reaction is the EMS's decision.

> A non-zero `ErrorCode` also drives `EVCS_State` (`30001`) to `4 = ERROR`. An EMS that treats over-temperature as informational should still expect the station state to follow it.

### Table C - Control pilot (CP) status codes

The CP signal (IEC 61851) indicates the EV connection state via letters A-F, encoded as `u16`.

| Code | IEC 61851 State | Meaning |
| --- | --- | --- |
| 0  | A  | Standby; no vehicle present.  |
| 1  | B  | Vehicle connected; EV not requesting charging.  |
| 2  | C  | Vehicle connected and requesting charging.  |
| 3  | D  | Charging permitted, ventilation required.  |
| 4  | E  | Error (EV/EVSE).  |
| 5  | F  | EVSE fault.  |
| 6-65535  | --  | Reserved / unknown.  |

### Table D - Connector State

| Code | Label | Meaning |
| --- | --- | --- |
| 0  | UNPLUGGED  | No plug in vehicle nor station.  |
| 1  | PLUGGED_ON_STATION_ONLY  | Plug latched at station, not in vehicle.  |
| 2  | PLUGGED_ON_STATION_AND_VEHICLE  | Plug inserted both sides; ready to lock/charge.  |
| 3  | ERROR_UNAVAILABLE  | Hardware error or connector unavailable.  |

### Table E - Charge Mode

| Code | Label | Meaning |
| --- | --- | --- |
| 0  | ECO  | Optimize for PV surplus; the EVCS derives charging current dynamically.  |
| 1  | POWER  | Charge at maximum possible power.  |
| 2  | MANUAL  | Fixed current set by user/EMS (A).  |

### Table F - Auth Type

| Code | Label | Meaning |
| --- | --- | --- |
| 0  | FREE  | No authentication required.  |
| 1  | RFID  | Local/online RFID authorization.  |
| 2  | KEYSWITCH  | Physical keyswitch required.  |
| 3  | OCPP  | Backend authorization via OCPP.  |

### Table G - Phase Switching Status

| Code | Label | Meaning |
| --- | --- | --- |
| 0  | IDLE  | No switching in progress.  |
| 1  | IN_PROGRESS  | Switching phases underway.  |

### Table H - Status Word Bitfield

| Bit | Label | Meaning |
| --- | --- | --- |
| 0-15  | RESERVED  | Reserved for future use; currently all `0`.  |

> StatusWord (`30003`) is not yet populated. For daemon health, use **DaemonStatus** (Table I).

### Table I - DaemonStatus Bitfield (IR 30050)

`DaemonStatus` exposes the daemon's own startup and connectivity state, distinct from the EVCS's electrical/charging state. Use it to tell "the charger is offline" apart from "the gateway is still starting up".

| Bit | Label | Meaning (when set to 1) |
| --- | --- | --- |
| 0  | units_ready  | Startup gate has opened; the gateway is ready to serve requests.  |
| 1  | mqtt_connected  | The internal data session to the charger is established.  |
| 2  | api_seed_succeeded  | At least one configuration seed has completed without error since daemon start (the bit is not cleared by a later failed re-seed).  |
| 3  | persistence_degraded  | A failsafe setting is enforced live but has not yet been saved to disk (a reboot would revert it).  |
| 4  | degraded_mode  | The gateway opened its request gate before all units finished startup.  |
| 5-15  | RESERVED  | For future use; always `0`.  |

---

## Controlling the charger

Charging is controlled by exactly two registers: `Set_Charge_Current_Target` (`40202`) and `Start_Stop_Charging` (`40203`). `Set_Charge_Power_Target` (`40201`) is accepted but does nothing.

### Starting a session

1. Write a current target to `40202` inside the station's window (`MinCurrent`-`MaxCurrent`, HR `40104`/`40105`).
2. Write `1` to `40203`.

A start command is **refused while the current target is** `0`: `40203 = 1` with `40202 = 0` returns exception `0x03` and nothing is applied. Writing both registers in a single FC16 frame is the safe pattern; that combination is checked before anything commits, so the frame is rejected as a whole rather than half-applying.

> Firmware older than this baseline reported this refusal as `0x04` (Slave Device Failure). If your client distinguishes the two, treat both as "set a valid current target first".

### The start/stop interlock on the current target

**While** `Start_Stop_Charging` is `0`, the charger is offered `0 A` no matter what you write to `40202`. The write is accepted and the register reads back the value you sent, but the station is told `0`. This is deliberate: raising the offered current while stopped could begin a charge nobody commanded.

The practical consequence is that a current target written before the start command does not take effect on its own - it is applied when `40203` goes to `1`. If you write a current target and see the charger stay at `0 A`, read `40203` first.

### Changing the current during a session

With `40203` already at `1`, write the new value to `40202`; it is offered to the vehicle immediately. Values outside the station's window are rejected with `0x03` and the register keeps its previous value.

### Stopping

Write `0` to `40203`. The charger is offered `0 A` and the session is de-authorised. `EVCS_State` leaves CHARGING once the station has actually stopped, within one telemetry cycle (**≤ 2 s**) - not at the moment the write is accepted.

### Charge mode matters

An EMS current target only holds while the station is in **MANUAL** mode (`ChargeMode = 2`). In ECO and POWER mode the station computes the charging current itself - from PV surplus in ECO, or at maximum in POWER - and will override a value written over Modbus. Before relying on `40202`, read the station's actual mode at `ChargeMode_Actual` (`30601`); if it is not `2`, write `2` to `ChargeMode` (`40107`) and confirm the change appears at `30601`.

> Changing charge mode - including switching _into_ manual - resets the station's charging limit. Re-write your current target after any mode change.

### Phase selection

`Target_Phase` (`40209`) accepts `1` or `3`, and only if phase switching is enabled on the station; otherwise the write is rejected with `0x03`. Phase switching takes several seconds - monitor `PhaseSwitchingStatus` (`30302`) and `ActualPhase` (`30303`). While it runs, `EVCS_State` reports `7 = PHASE_SWITCHING`.

---

## Functional examples

### Reading identity information (single shot)

**Goal**: read `MapVersion`, `Model`, `SerialNumber`, `Manufacturer`, `FirmwareVersionStr`, `FirmwareVersionU32` in one request.

**Where**: HR `40001 ... 40058` - that is **58 registers** starting at PDU offset `0`.

**Function**: `0x03` (Read Holding Registers).

**Request PDU** (start at 0, quantity 58 = `0x003A`):

```
03 00 00 00 3A
```

**Response**: byte count + **116 bytes** (58 regs x 2 bytes). Decode 2 ASCII chars per register for strings and trim trailing `0x00`. The last two registers are the `u32` firmware number (word order HighWord-LowWord, big-endian bytes within each register).

### Reading metering values (per-phase active power)

**Goal**: read total + L1/L2/L3 active power (four `f32`).

**Where**: IR `31015 / 31017 / 31019 / 31021` -> PDU offsets `1014 / 1016 / 1018 / 1020`.

**Function**: `0x04` (Read Input Registers).

**Request PDU** (read 8 regs starting at 1014):

```
04 03 F6 00 08
```

**Response**: byte count + **16 bytes** = 4 x `f32` in order `Power_Total`, `Power_L1`, `Power_L2`, `Power_L3` (each spans two regs, HighWord at the lower address).

### Setting a charging current target (e.g. 16 A)

**Goal**: write `Set_Charge_Current_Target` (HR `40202`, PDU offset `201`) with **16 A**.

> This register is a `u16`, not a float. Valid values are `0` (stop) or any current inside the station's window (`MinCurrent`-`MaxCurrent`, HR `40104`/`40105`). Write the integer amp value directly.

**Function**: `0x06` (Write Single Register).

**Request PDU** (`address = 0x00C9` = 201, `value = 0x0010` = 16):

```
06 00 C9 00 10
```

**Response**: echo of address and value (`00 C9 00 10`). Values outside `0` or the station's window are rejected with exception `0x03` and the register is unchanged. Remember that the value is only offered to the charger once `Start_Stop_Charging` (`40203`) is `1` - see **Controlling the charger**.

### Setting current and starting charging atomically (FC16)

**Goal**: set the current target to 16 A and start charging in one atomic frame, avoiding the "start with current target = 0" failure.

**Where**: HR `40202-40203` -> PDU offsets `201-202` (`Set_Charge_Current_Target`, `Start_Stop_Charging`).

**Function**: `0x10` (Write Multiple Registers).

**Request PDU** (`start = 0x00C9` = 201, `quantity = 2`, `byte count = 4`, values `16`, `1`):

```
10 00 C9 00 02 04 00 10 00 01
```

**Response**: echo of start and quantity (`00 C9 00 02`).

> Because FC16 is atomic, this avoids issuing a start command while the current target is still `0` (which the device rejects with exception `0x03`; firmware older than this baseline reported `0x04` for this). It also side-steps the start/stop interlock: written together, the current target is offered as soon as the start command commits. `Set_Charge_Power_Target` (`40201`) is not implemented; charging is controlled solely by `Set_Charge_Current_Target` (`40202`) and `Start_Stop_Charging` (`40203`).

### Entering (and leaving) failsafe

**Behavior**: if EMS traffic stops, the EVCS uses `Failsafe_Timeout` to decide when to apply the failsafe current.

**Relevant registers**

* `40204` (PDU 203) `Failsafe_Timeout` (s) - RW; `0` disables; non-zero must be `10-3600`; default `20`.
* `40206` (PDU 205) `Failsafe_Current_Target` (A, `u16`) - RW; `0`, or a current inside the station's window (`MinCurrent`-`MaxCurrent`, HR `40104`/`40105`).
* `30027` (PDU 26) `Failsafe_Active` (`0/1`) - **read-only Input register** (read via FC04).
* `40208` (PDU 207) `Heartbeat` - write-only; write any value periodically.
* `30004` (PDU 3) `ErrorCode`.

**Flow**

1. EMS configures `Failsafe_Current_Target` and `Failsafe_Timeout`, then periodically writes the `Heartbeat` (`40208`) - only accepted writes to `40202`, `40203`, `40208` or `40209` reset the timer.
2. If no timer-resetting write arrives within `Failsafe_Timeout` seconds, the EVCS enters failsafe: it applies `Failsafe_Current_Target`, sets `Failsafe_Active = 1` (read at IR `30027`), and reports `ErrorCode = 7` (Failsafe active). If the charger cannot apply the failsafe current, `Failsafe_Active` stays `0` and the device retries.
3. When EMS resumes communication, the EVCS exits failsafe: it first restores the client's intended current (based on `Start_Stop_Charging`), then clears `Failsafe_Active = 0`. `ErrorCode` returns to its normal value.

**Configuration persistence (live-first)**

* Writes to `Failsafe_Timeout` (`40204`) and `Failsafe_Current_Target` (`40206`) take effect **immediately** and are **never rolled back**; a read-back always equals the value in force.
* Saving the value to permanent storage is best-effort. If it has not yet been saved, **DaemonStatus bit 3** (`persistence_degraded`, IR `30050`) is set, and a reboot in that window restores the previous saved value. If a change must survive an immediate power-cycle, confirm bit 3 is `0` after the write.

---

## Data encoding rules

* **Strings**: each 16-bit register carries two ASCII characters (MSB = first). Strings are not null-terminated; unused bytes are `0`. Trim trailing zeros. Example: registers `0x6350 0x4832` decode to `cPH2`.
* **float32**: IEEE-754 32-bit floats are transmitted big-endian with the **high word at the lower address**. Assemble both registers before decoding. Example: `16.0` -> `0x41800000` -> high word `0x4180`, low word `0x0000`.
* **uint32**: big-endian byte and word order, high word at the lower address. Used for `Uptime` and `Temperature` (integer degrees C). Example: `0x0203004B` -> high word `0x0203`, low word `0x004B`.
* **uint16 / boolean / enum**: a single big-endian register; booleans use `0 = false`, `1 = true`; enums use the values in the tables above (treat unrecognised codes as reserved).

---

## Timing rules and recommendations

* **Telemetry cadence**: the EVCS updates metering and state registers at least once every **5 s**. Avoid polling faster than the update rate; cache values where possible.
* **Setpoint / heartbeat cadence**: refresh the heartbeat (or another timer-resetting write) more often than the configured `Failsafe_Timeout`. A safe rule is at most **timeout / 2**: with the default 20 s timeout, write at least every **10 s**; with the minimum 10 s timeout, write at least every **5 s**. Writes to `Set_Charge_Current_Target`, `Start_Stop_Charging`, `Target_Phase`, and the dedicated `Heartbeat` register all reset the failsafe timer.
* **Response time**: 95 % of Modbus responses are returned within **200 ms** under normal conditions.
* **Coherent snapshots**: multi-register reads return data from a consistent snapshot. Avoid mixing register ranges that update at different times (e.g. reading identity and power in the same request).
* **Readiness gating**: before starting automation after a charger reboot, poll DaemonStatus (`30050`) and wait for bit 0 (`units_ready`).

---

## Error handling

* Invalid function codes, addresses, or values produce Modbus exception responses: `0x01` Illegal Function, `0x02` Illegal Data Address, `0x03` Illegal Data Value, `0x04` Slave Device Failure, `0x06` Server Device Busy, `0x0A` Gateway Path Unavailable (proxied slaves only), `0x0B` Gateway Target Device Failed to Respond.
* **Writes** follow this matrix:

    * Write to a read-only register, or a value outside the valid range -> `0x03` (the register is unchanged; no clamping).
    * Write to a valid-but-not-writable address (e.g. `40108`, or a gap) -> `0x02`.
    * Misaligned / wrong-length multi-register frame -> `0x03`.
    * Start charging (`40203 = 1`) while `Set_Charge_Current_Target` is `0` -> `0x03` (set a valid current target first; an FC16 frame combining both is the safe pattern). Firmware older than this baseline reported `0x04` for this.
    * An FC16 frame that would leave `MinCurrent` above `MaxCurrent` -> `0x03`, with no register in the frame changed.
    
* Out-of-range values are **rejected**, not clamped: the register keeps its previous value, so re-reading it after a rejected write shows no change.

### Troubleshooting

| Symptom | Likely cause | What to check |
| --- | --- | --- |
| Cannot connect at all (TCP reset / refused, no Modbus reply)  | Your IP is not on the allow-list, or a connection limit was hit  | Confirm your source IP is allow-listed; check you are within 4 connections/IP and 32 total  |
| Exception `0x06`  | Rate-limited, or daemon still starting up  | Back off and retry; poll DaemonStatus (`30050`) bit 0  |
| Exception `0x0B`  | Wrong Unit ID  | Use the configured Unit ID (default `1`)  |
| Write returns `0x03`  | Value out of range, or target is read-only  | Send a value within the documented bounds; do not write read-only registers  |
| Write returns `0x02`  | Address is not writable  | Write only the writable ranges (`40104-40107`, `40201-40225`)  |
| Current target rejected with `0x03` although the value is within `6-32`  | The station's window is narrower than the hardware range  | Read `MinCurrent` (`40104`) and `MaxCurrent` (`40105`) - writes to `40202`/`40206` are validated against that window, not against `6-32`  |
| Start charging returns `0x03` (`0x04` on older firmware)  | Current target is `0` at start  | Set `Set_Charge_Current_Target` within the station's window first, or use one atomic FC16 frame  |
| Current target accepted, but the charger stays at `0 A`  | `Start_Stop_Charging` is `0`, so the offered current is forced to `0`  | Write `1` to `40203`, or write both registers in one FC16 frame  |
| Current target accepted and charging runs, but the current drifts to another value  | The station is in ECO or POWER mode and is computing the current itself  | Read `ChargeMode_Actual` (`30601`); set `ChargeMode` (`40107`) to `2` (MANUAL) and re-write the target  |
| A setpoint write to `40106`/`40107` reads back correctly but nothing changes  | Those registers are EMS setpoints; the station may be running something else  | Compare against `EcoPlus_Actual` (`30602`) and `ChargeMode_Actual` (`30601`)  |
| Charging will not start, all setpoints look correct  | Charging is paused locally from the web interface or app  | Read `PauseCharging_Actual` (`30603`); a local pause overrides an EMS start  |
| `ErrorCode = 4` will not clear  | Station is still above the clear threshold  | It clears itself once the electronics cool 5 K below the threshold; no restart is needed. Watch `Temperature` (`30025`)  |
| `EVCS_State = 4` (ERROR) while the station charges normally  | Any non-zero `ErrorCode` masks the charging state  | Read `ErrorCode` (`30004`); an informational code such as `4` still forces `EVCS_State` to ERROR  |
| `EVCS_State` shows CHARGING for a session the EMS did not start  | Expected since `MapVersion 2.1.0`  | CHARGING reflects energy flow, whoever started it; read `Start_Stop_Charging` (`40203`) for your own command state  |
| `ErrorCode = 7` and `Failsafe_Active = 1`  | EMS communication stopped past the timeout  | Resume the heartbeat; the device restores the intended current then clears the flag  |
| Temperature reads as a huge/garbage number  | Decoded as float  | Decode `30025` as a big-endian `u32` (integer degrees C)  |

---

## Document history

| Version | Date | Map version | Notes |
| --- | --- | --- | --- |
| 2.1.0  | 2026-08-20  | 2.1.0  | **Corrected**: `MapVersion` is `2.1.0`; starting with a zero current target returns `0x03`, not `0x04` (four places, including the exception table and the troubleshooting matrix); the `0x04` description no longer implies a bad value; `Set_Charge_Current_Target` and `Failsafe_Current_Target` are bounded by the station's `MinCurrent`-`MaxCurrent` window rather than a fixed `6-32`. **Added**: the **Actual configuration state** block (IR `30601-30603`); a **Controlling the charger** section covering the start/stop interlock on `40202`, the MANUAL-mode requirement, and phase selection; a **How the state signals are derived** section with the `EVCS_State` and `Connector_State` condition tables; the error-code detection conditions; the FC16 rule rejecting `MinCurrent > MaxCurrent`; nine troubleshooting rows. **Changed**: `EVCS_State = CHARGING` no longer requires the EMS start command; over-temperature (`ErrorCode 4`) is judged from the live reading with 5 K hysteresis, clears without a restart, and uses a per-station threshold read from the hardware instead of a fixed value. **Clarified**: `MinCurrent`/`MaxCurrent` track the station, `EcoPlus`/`ChargeMode` are EMS setpoints only, and `ErrorCode 4` is informational but still forces `EVCS_State` to ERROR.  |
| 2.0.1  | 2026-07-29  | 2.0.0  | Corrected the ErrorCode PDU offset in the failsafe example (`3`, not `4`); corrected the FirmwareVersionU32 example to `0x0203004B`; stated the exact set of failsafe timer-resetting writes (`40202`/`40203`/`40208`/`40209`); corrected the Plug_Error and Contactor_Error notes (firmware fault codes, not Table B); corrected DaemonStatus bit 2 (since-start latch); corrected six block ranges to the last occupied register; corrected Failsafe_Power_Target persistence to `VOL`; replaced the power/current mode-selection advice; clarified write-only reads and reserved-entry listing.  |
| 2.0.0  | 2026-06-16  | 2.0.0  | Corrected Failsafe_Active to IR `30027`; rewrote the configuration block (correct addresses, writable setpoints); corrected Temperature to `u32`; added the Security section, DaemonStatus (`30050`), the full exception set (`0x06`/`0x0A`/`0x0B`), error priority, the Mains block availability, the FC16 example, and a troubleshooting matrix; clarified reject-not-clamp and scaling.  |
| 1.0.0  | --  | 1.0.0  | Initial release (superseded).  |

---

_(c) eCharge Hardy Barth GmbH - Modbus TCP Documentation_
