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 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.
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).
You do not need the bridge for normal use. Linking Fox Cloud is enough for charts, history, forecasts, and notifications.
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 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 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.
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 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
- 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.
- 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 (The Pi Hut)
- Elfin EW11 - compact WiFi-to-RS485 with Modbus TCP
- Fox ESS inverter with Modbus TCP enabled on the adapter
- Check My Solar account with the inverter already linked via Fox Cloud
- Network access from the Docker host to the Modbus adapter IP
You can test Modbus connectivity before setting up the bridge - from your host or from Docker (see Test inverter connectivity below).
Setup
Create a bridge token in the app
- Open Menu → Account
- Tap Modbus Bridge
- Make sure the correct inverter is selected on the dashboard first - each account can have one active bridge, tied to one device
- 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
On a machine on your LAN, clone the modbus-bridge repository:
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 -dThe compose stack runs two containers:
- modbus - polls the inverter and serves
GET /v1/realtimeon 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):
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 probeA 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):
docker run --rm \
-e MODBUS_HOST=192.168.1.100 \
ghcr.io/checkmysolar/modbus-bridge:latest \
dist/probe.jsOptional 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:
docker compose exec modbus dist/probe.jsIf 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.
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:
- The app loads Fox Cloud data as usual for charts
- If a bridge is active, it also polls the Modbus endpoint in the background
- When bridge data is fresh (sampled within the last 60 seconds), live fields on the energy flow diagram are updated from the bridge
- 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:
MODBUS_READ_ONLY=falseAdd it to your Docker environment (for example in .env or docker compose overrides), then restart the modbus container.
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
cloudflaredcontainer and Check My Solar travels over a 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 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):
cd modbus-bridge
docker compose pull modbus
docker compose up -d modbusDocker run:
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 beforeYour .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 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.