---
title: "Fox ESS Modbus Bridge"
description: "Set up the optional Fox ESS Modbus Bridge for faster live telemetry in Check My Solar - Docker stack, Cloudflare Tunnel, and Today view updates every 10 seconds."
---

> Documentation Index
> Fetch the complete documentation index at: https://checkmy.solar/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Fox ESS Modbus Bridge

import { ModbusBridgeArchitectureDiagram } from "@/components/react/ModbusBridgeArchitectureDiagram";

By default, Check My Solar reads live data from Fox Cloud the same path as the Fox ESS app. That works well, but Fox Cloud updates up to every 5 minutes, which can make the Today view feel a little slow.

The Modbus Bridge is an optional add-on for Fox ESS inverters. A small [Docker stack](https://github.com/checkmysolar/modbus-bridge) on your home network reads the inverter directly over Modbus TCP, then Check My Solar pulls that snapshot through a secure private tunnel. When the bridge is online, the dashboard can refresh live numbers about every **10 seconds** on the Today view, and immediately after opening the app.

:::note[Read-only by default]
The bridge only **reads** inverter registers unless you opt in. Set `MODBUS_READ_ONLY=false` if you want to change work mode from the app. See [Read-only mode and work mode control](#read-only-mode-and-work-mode-control).
:::

---

## When to use it

The Modbus Bridge is a good fit if you:

- Have a **Fox ESS** inverter with **Modbus TCP** enabled (typically via an RS485-to-Ethernet adapter on your LAN)
- Run a home server, NAS, or always-on machine that can host Docker
- Want **faster live updates** on the Today dashboard without waiting on Fox Cloud polling

### Supported inverters

The bridge auto-detects the register profile from your inverter model string. Supported families include:

| Profile | Example models |
|---------|----------------|
| H1 G2 | H1-G2, AC1-G2, P1 |
| H1 / AC1 G1 | H1, AC1, AIO-H1 |
| KH | KH10.5, KH-5.0 |
| H3 (legacy) | H3, AC3, AIO-H3, Kuara, SK-HWR, STAR, Solavita SP |
| H3 Pro / Smart / EVO | H3-Pro, H3-Smart, P3-SH, Enpal I-X, 1KOMMA5, EVO |

If auto-detect fails, set `INVERTER_PROFILE` manually (see [Environment variables](#environment-variables)).

You do **not** need the bridge for normal use. Linking Fox Cloud is enough for charts, history, forecasts, and notifications.

:::note[Availability]
The Modbus Bridge is an advanced, opt-in feature. It may be behind a feature flag in your account - if you do not see **Modbus Bridge** under **Menu → Account**, message on [Discord](https://discord.gg/9ZS5DBMvts).
:::

---

## Architecture overview

At a high level, data flows from your inverter to the app like this:

**Protocols along the path:** Modbus TCP between inverter and bridge; HTTP `GET /v1/realtime` on your Docker network; outbound [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/) to Check My Solar’s private backend.

**Latency:** End-to-end from a Modbus read on your LAN to the value appearing on the Today dashboard is typically about **100 ms**-inverter → bridge → tunnel → Check My Solar backend → app. The dashboard polls about every **10 seconds** while you are on Today, so you get fresh numbers quickly.

### Components

| Component | Where it runs | Role |
|-----------|---------------|------|
| **Inverter** | Your home | Source of truth for live power, battery, grid, and load readings |
| **Modbus Bridge** | Docker on your LAN | Polls the inverter over Modbus TCP, stores the latest snapshot locally |
| **cloudflared** | Docker on your LAN | Connects the bridge into Check My Solar’s private network via a [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/) secured with TLS 1.3 and post-quantum key agreement |
| **Check My Solar backend** | Cloudflare Workers | Reaches your bridge over a private VPC connection (not the public internet) |
| **Dashboard** | Web or iOS | Requests live telemetry when you are on the **Today** view and the bridge is healthy |

### Live variables replaced

When the bridge is online, these **Today** live fields come from Modbus instead of Fox Cloud:

| Variable | What it shows |
|----------|---------------|
| `pvPower` | Total solar generation |
| `pv1Power`, `pv2Power` | PV string 1 and 2 power |
| `pvStringCount`, `pvStringPowers` | Number of PV strings and per-string power |
| `SoC` | Battery state of charge |
| `ResidualEnergy` | Remaining battery energy |
| `batChargePower` | Battery charge power |
| `batDischargePower` | Battery discharge power |
| `batVoltage` | Battery voltage |
| `batCurrent` | Battery current |
| `batTemperature` | Battery temperature |
| `gridConsumptionPower` | Grid import |
| `feedinPower` | Grid export |
| `gridVoltage` | Grid voltage |
| `gridCurrent` | Grid current |
| `gridFrequency` | Grid frequency |
| `meterPower2` | Secondary meter (CT clamp 2) |
| `loadsPower` | House load |
| `ambientTemperature` | Ambient temperature |
| `deviceTemperature` | Inverter temperature |
| `runningState` | Inverter running state |
| `isOffGrid` | Off-grid status (derived from running state) |
| `epsPower`, `epsPowerR`, `epsVoltR`, `epsCurrentR` | EPS / backup circuit (phase R) |
| `workMode` | Work mode (Self-use, Feed-in priority, etc.) |

### Today’s totals replaced

When the bridge is online, these **Today** kWh totals also come from Modbus instead of Fox Cloud:

| Total | What it shows |
|-------|---------------|
| `generation` | Solar generation today |
| `feedin` | Grid export today |
| `gridConsumption` | Grid import today |
| `chargeEnergyToTal` | Battery charge today |
| `dischargeEnergyToTal` | Battery discharge today |
| `loadConsumption` | House load today |

If the bridge goes offline or data goes stale, the app shows a status message and does **not** fall back to Fox Cloud for these fields by default - historical charts still load from Fox as usual. You can enable **Fox Cloud fallback** in **Menu → Account → Modbus Bridge** to use Fox Cloud live data when the bridge is unreachable.

When the bridge is online, **work mode** on Today also comes from Modbus (instead of Fox device settings). You can open the **Work mode** modal from the Today view to see what each mode does. Changing work mode from the app requires a read-write bridge - see [Read-only mode and work mode control](#read-only-mode-and-work-mode-control).

### What stays on Fox Cloud

The bridge only replaces **live telemetry and today’s totals** for the current day. Everything else still comes from Fox Cloud:

- Historical charts (week, month, year)
- Daily totals for past days and other time views
- Export limit, scheduler, and most other device settings
- Forecasts and notifications (Fox Cloud users rely on periodic polling; [Modbus Bridge](/docs/using-the-app/modbus-bridge/) users get near-instant battery, grid, and mode alerts)

Work mode **changes** from the app are only available when the bridge is configured for read-write access (`MODBUS_READ_ONLY=false`). Otherwise the app still shows the live work mode from Modbus, but cannot write a new mode.

---

## Prerequisites

1. **Home server capable of running Docker** - an always-on machine on the same LAN as your inverter (home server, NAS, Raspberry Pi, etc.). It only needs to run the small Modbus Bridge Docker stack.
2. **RS485 adapter** - connects your inverter’s RS485 port to your network as Modbus TCP (default port **502**, unit ID **247**). Examples community members use:
- [Industrial Serial Server RS485 to RJ45 Ethernet](https://thepihut.com/products/industrial-serial-server-rs485-to-rj45-ethernet) (The Pi Hut)
- [Elfin EW11](https://www.cnx-software.com/2023/03/06/elfin-ew11-is-a-compact-wifi-to-rs485-unit-with-modbus-tcp-support/) - compact WiFi-to-RS485 with Modbus TCP
3. **Fox ESS** inverter with Modbus TCP enabled on the adapter
4. **Check My Solar account** with the inverter already linked via Fox Cloud
5. Network access from the Docker host to the Modbus adapter IP

:::tip[Multi-host / multi-user mode]
Some adapters (for example certain **Waveshare** models) allow only one Modbus TCP client at a time unless you enable **multi-host** or **multi-user** mode in the adapter’s web interface. Look for a setting named **multi-host function** or similar. Without it, the Modbus Bridge may fail to connect if another client is holding the bus.
:::

You can test Modbus connectivity before setting up the bridge - from your host or from Docker (see [Test inverter connectivity](#test-inverter-connectivity) below).

---

## Setup

1. **Create a bridge token in the app**

   1. Open **Menu → Account**
   2. Tap **Modbus Bridge**
   3. Make sure the correct **inverter** is selected on the dashboard first - each account can have **one active bridge**, tied to one device
   4. Tap **Create token** and copy the secrets shown **immediately** - they are only displayed once

   The modal shows:

- **Bridge token** - authenticates the local HTTP API (`CMS_BRIDGE_TOKEN`)
- **Tunnel run token** - starts `cloudflared` (`TUNNEL_TOKEN`)
- **Private hostname** - must match the bridge configuration (`BRIDGE_HOSTNAME`)
   - A ready-to-run **docker compose** command
2. **Run the Docker stack at home**

   On a machine on your LAN, clone the [modbus-bridge](https://github.com/checkmysolar/modbus-bridge) repository:

```bash
git clone https://github.com/checkmysolar/modbus-bridge.git
cd modbus-bridge
export CMS_BRIDGE_TOKEN='cms_bridge_...'          # from the app
export MODBUS_HOST='192.168.1.100'              # your Modbus adapter IP
export BRIDGE_HOSTNAME='bridge-....modbus.internal'  # from the app
export TUNNEL_TOKEN='eyJ...'                       # from the app
# Optional: set MODBUS_READ_ONLY=false to allow work mode changes from the app (default is true)
docker compose up -d
```

   The compose stack runs two containers:

- **modbus** - polls the inverter and serves `GET /v1/realtime` on port 8080
- **cloudflared** - connects the bridge to Check My Solar’s private network

   No inbound ports need to be opened on your router. Traffic flows **outbound** from `cloudflared` through the tunnel, encrypted with TLS 1.3 and hybrid post-quantum key agreement on the connection to Cloudflare’s network.

   The bridge container uses a **distroless** runtime image (no shell or package manager), runs as a non-root user, and is not exposed on your LAN — only `cloudflared` on the internal Docker network can reach it on port 8080.

   #### Test inverter connectivity

   Before creating bridge tokens, confirm the inverter is reachable over Modbus from your LAN.

   **From the host** (clone the repo, no Docker):

```bash
git clone https://github.com/checkmysolar/modbus-bridge.git
cd modbus-bridge
npm install
npm run build
MODBUS_HOST=192.168.1.100 npm run probe
```

   A successful run prints the detected model and profile, live power, battery, and today’s energy counters, then `OK - inverter Modbus connectivity works`.

   **From Docker** (one-off; no cloud tokens or compose stack required):

```bash
docker run --rm \
  -e MODBUS_HOST=192.168.1.100 \
  ghcr.io/checkmysolar/modbus-bridge:latest \
  dist/probe.js
```

   Optional environment variables: `MODBUS_PORT` (default `502`), `MODBUS_UNIT_ID` (default `247`), `MODBUS_TIMEOUT_MS` (default `5000`).

   If the stack is already running, probe inside the `modbus` container:

```bash
docker compose exec modbus dist/probe.js
```

   If the probe fails, check adapter IP, port, unit ID, and that nothing else is holding the Modbus bus. The probe prints a short checklist on error.
3. **Verify in the app**

   Reopen **Modbus Bridge** in Account settings. The status banner should show:

   | Status | Meaning |
   |--------|---------|
   | **Online** | Bridge is reachable and data was sampled within the last 60 seconds |
   | **Stale** | Bridge was reachable but the last sample is older than 60 seconds |
   | **Waiting** | Token exists but Check My Solar has not received data from the bridge yet |

   On the **Today** dashboard, live numbers should update about every 10 seconds while the bridge is online.

---

## How the dashboard uses bridge data

When you view **Today**:

1. The app loads Fox Cloud data as usual for charts
2. If a bridge is active, it also polls the Modbus endpoint in the background
3. When bridge data is **fresh** (sampled within the last 60 seconds), live fields on the energy flow diagram are updated from the bridge
4. Polling stops automatically if data goes stale or you switch away from Today

---

## Read-only mode and work mode control

By default, the bridge runs in **read-only** mode (`MODBUS_READ_ONLY=true`). It polls inverter registers for live telemetry and today’s totals, but **never writes** holding registers. That is the safest default: Check My Solar can read your inverter faster without the bridge being able to change settings.

### What read-only affects

| Capability | Read-only (`true`, default) | Read-write (`false`) |
|------------|----------------------------|----------------------|
| Live telemetry on Today | Yes | Yes |
| Today’s kWh totals from Modbus | Yes | Yes |
| Work mode shown on Today | Yes (from Modbus) | Yes (from Modbus) |
| Change work mode from the app | No | Yes (supported profiles) |

### Enable work mode changes

To let Check My Solar change work mode through the bridge, set:

```bash
MODBUS_READ_ONLY=false
```

Add it to your Docker environment (for example in `.env` or `docker compose` overrides), then restart the **modbus** container.

:::caution[Writes change inverter behaviour]
Read-write mode allows the bridge to send Modbus commands (for example switching between Self Use and Force Charge). Only enable this if you want remote work mode control from Check My Solar. Most users can leave read-only enabled.
:::

### In the app

- **Menu → Account → Modbus Bridge** shows **Access** as **Read-only** or **Read and write** for the connected inverter.
- On **Today**, tap the work mode label to open the **Work mode** modal. If the bridge is read-only, you can still read about each mode, but **Set** buttons are hidden.
- Work mode control requires bridge version **1.4.0** or later and a supported inverter profile (H1 G2, KH, H3 Pro/Smart/EVO over LAN, and others as listed in the bridge release notes).

---

## Security and privacy

- **Post-quantum encryption** - Traffic between your home `cloudflared` container and Check My Solar travels over a [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/) protected by **TLS 1.3 with hybrid post-quantum key agreement** (X25519 + ML-KEM).
- The bridge token is a **bearer secret** - treat it like a password. Store it only on your home server.
- Check My Solar stores an **encrypted** copy server-side so the backend can authenticate to your bridge; the plain token is shown only once at creation.
- Communication from Check My Solar to your bridge uses a **private Cloudflare Tunnel hostname** (`*.modbus.internal`), not a public URL on your home IP.
- Your inverter is never exposed directly to the internet - only the bridge container talks to it on your LAN.
- **Read-only by default** - unless you set `MODBUS_READ_ONLY=false`, the bridge cannot write Modbus registers, so it cannot change work mode or other inverter settings from Check My Solar.
- Revoking a token in the app tears down the tunnel and invalidates access.

See [Privacy & data](/docs/reference/privacy/) for how Check My Solar handles account and device data generally.

---

## Managing tokens

From **Menu → Account → Modbus Bridge** you can:

- **Create** a token for the selected inverter (one active bridge per account)
- **Revoke** a token - disconnects the tunnel and stops bridge access
- Check **bridge status** and last sample time
- See **connected inverter** details, including bridge version, firmware, and **Access** (read-only vs read-write)
- Toggle **Fox Cloud fallback** when the bridge is unreachable

---

## How to update

When the app shows a bridge update is available, pull the latest image and restart the **modbus** container. You do **not** need a new bridge token or tunnel token.

**Docker Compose** (recommended):

```bash
cd modbus-bridge
docker compose pull modbus
docker compose up -d modbus
```

**Docker run**:

```bash
docker pull ghcr.io/checkmysolar/modbus-bridge:latest
docker stop modbus-bridge && docker rm modbus-bridge
# Re-run your original docker run command from the app, or use the same env/volume flags as before
```

Your `.env`, tunnel, and `bridge_data` volume are unchanged. After restart, reopen **Menu → Account → Modbus Bridge** and confirm the **Bridge** version matches the latest release.

---

## Troubleshooting

| Symptom | Things to check |
|---------|-----------------|
| **Waiting** status never clears | Docker containers running? `MODBUS_HOST` correct? Adapter reachable from the Docker host? |
| **Stale** data | Inverter online? Modbus polling errors in bridge logs? Poll interval default is 10 seconds |
| No **Modbus Bridge** menu item | Feature may not be enabled for your account yet |
| Bridge on wrong device | Only one active token per account - revoke the old one before creating for another inverter |
| Reinstalled Docker | Revoke the old token and create a new one - secrets are only shown once at creation |
| Modbus errors in bridge logs | Run `docker run --rm -e MODBUS_HOST=... ghcr.io/checkmysolar/modbus-bridge:latest dist/probe.js`, or `docker compose exec modbus dist/probe.js` if the stack is already up. For H1 G1 over RS485, set `MODBUS_CONNECTION=aux`. Override profile with `INVERTER_PROFILE` if auto-detect fails. |
| Cannot change work mode from the app | Bridge shows **Read-only** in Modbus Bridge settings? Set `MODBUS_READ_ONLY=false` and restart the modbus container. Also check bridge version is 1.4.0+ and your inverter profile supports writes. |

---

## Environment variables

| Variable | Default | Purpose |
|----------|---------|---------|
| `CMS_BRIDGE_TOKEN` | *(required)* | Authenticates the local HTTP API |
| `MODBUS_HOST` | *(required)* | RS485 adapter or inverter IP |
| `BRIDGE_HOSTNAME` | *(required)* | Private tunnel hostname from the app |
| `TUNNEL_TOKEN` | *(required)* | `cloudflared` run token from the app |
| `MODBUS_PORT` | `502` | Modbus TCP port |
| `MODBUS_UNIT_ID` | `247` | Modbus unit ID |
| `MODBUS_CONNECTION` | `aux` | `aux` for RS485 adapters; `lan` for direct inverter LAN |
| `INVERTER_PROFILE` | *(auto)* | Force profile: `h1g2`, `h1Series`, `kh`, `h3Legacy`, `h3Modern` |
| `POLL_INTERVAL_MS` | `10000` | Poll interval in milliseconds |
| `MODBUS_TIMEOUT_MS` | `5000` | Modbus read timeout |
| `MODBUS_READ_ONLY` | `true` | When `true`, only read registers; work mode and other writes are disabled. Set to `false` to allow writes. |
| `CMS_API_BASE_URL` | `https://checkmy.solar` | Check My Solar API origin used for instant notification triggers |
| `CMS_NOTIFICATIONS_ENABLED` | `true` | When `false`, the bridge does not POST notification triggers |
| `BRIDGE_VERBOSE_LOG` | `false` | Log each poll and HTTP request |

---

## What the bridge does not provide

Historical charts and daily totals for past days always come from Fox Cloud regardless of bridge status.

---

## FAQ

### How does the Docker stack work?

`docker compose up` starts **two containers** on a private Docker network (`cms_net`):

| Container | Role |
|-----------|------|
| **modbus** | Polls your inverter over Modbus TCP on the LAN, stores the latest snapshot, and serves `GET /v1/realtime` on port 8080 |
| **cloudflared** | Maintains an outbound [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/) connection so Check My Solar can reach the bridge without opening inbound ports on your router |

The **modbus** container is the only one that talks to your inverter. It needs `MODBUS_HOST` set to your RS485-to-Ethernet adapter IP on your home network.

When you create a token in the app, Check My Solar also provisions a Cloudflare Tunnel and a **private hostname route** for your account. You copy the tunnel run token and hostname into your environment variables and start the stack at home.

### How does the tunnel reach the bridge?

The private hostname (for example `bridge-abc123….modbus.internal`) is registered as a **Docker network alias** on the `modbus` container. Both containers share the same `cms_net` network, so `cloudflared` can resolve that hostname to the bridge container’s internal IP.

When Check My Solar needs live data, its backend calls your private hostname over Cloudflare’s private network. The request arrives at your home `cloudflared` container through the tunnel, and `cloudflared` forwards it to `http://<BRIDGE_HOSTNAME>:8080` on the Docker network - not to a public IP or a port on your router.

### Can I access the bridge from my LAN?

**Not by default.** We do not expose the bridge HTTP API on your home network. Port 8080 is only reachable on the internal Docker network between `modbus` and `cloudflared`, and Check My Solar reaches it through the encrypted tunnel.

You can expose it on your LAN if you want to - for example, by adding a `ports` mapping to the `modbus` service in `docker-compose.yml` so other devices on your network can call `GET /v1/realtime`. That is optional and not required for Check My Solar.

### Can I change the private hostname?

**No.** The hostname is generated when you create a bridge token and is tied to your Check My Solar account and that token’s tunnel configuration. It always ends in `.modbus.internal`.

Use the exact value shown in **Menu → Account → Modbus Bridge** for `BRIDGE_HOSTNAME`. If you revoke the token and create a new one, you get a new hostname and new secrets - update your Docker environment and restart the stack.

### Why are there two containers instead of one?

They do different jobs:

- **modbus** - inverter polling and the local HTTP API
- **cloudflared** - secure outbound tunnel only; it does not talk to your inverter directly

### Do I need to open ports on my router?

**No.** All traffic from Check My Solar to your bridge flows **outbound** from `cloudflared` through the tunnel. You do not need port forwarding, a static public IP, or inbound firewall rules for the bridge.

### How fast is it?

End-to-end latency is typically about **100 ms** from the inverter to your dashboard: Modbus read on your LAN → local bridge snapshot → Cloudflare Tunnel → Check My Solar backend → Today view. That is much faster than Fox Cloud live data, which can lag by up to several minutes.

While you are on Today with a healthy bridge, the app polls for new snapshots about every **10 seconds**, so live power, battery, and grid numbers stay current without hammering your inverter or home network.

### Why is the bridge read-only by default?

Read-only mode means the bridge only **reads** Modbus registers. Check My Solar gets faster live data without the bridge being able to change inverter settings. If you want to change work mode from the app, set `MODBUS_READ_ONLY=false` on the bridge container and restart - see [Read-only mode and work mode control](#read-only-mode-and-work-mode-control).

---

## Related pages

- [Dashboard overview](/docs/using-the-app/dashboard-overview/)
- [Time views (Today, Week, Month)](/docs/using-the-app/time-views/)
- [Real-time production via Fox Cloud](/docs/guides/real-time-solar-production-fox-ess/)
- [Account & settings](/docs/using-the-app/account-settings/)
- [Discord community](https://discord.gg/9ZS5DBMvts)

Source: https://checkmy.solar/docs/using-the-app/modbus-bridge/index.mdx
