Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ celerybeat.pid

# Environments
.env
docker-compose/alertmanager/alertmanager.yml
.venv
env/
venv/
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ FROM python:3.12-alpine

LABEL org.opencontainers.image.authors="Yaroslav Berezhinskiy <yaroslav@berezhinskiy.name>"
LABEL org.opencontainers.image.description="An implementation of a Prometheus exporter for EcoFlow portable power stations"
LABEL org.opencontainers.image.source=https://github.com/berezhinskiy/ecoflow_exporter
LABEL org.opencontainers.image.source=https://github.com/serhiioliinyk/ecoflow-exporter
LABEL org.opencontainers.image.licenses=GPL-3.0

RUN apk update && apk add py3-pip
Expand Down
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Unlike REST API exporters, it is not required to request for `APP_KEY` and `SECR
The project provides:

- [Python program](ecoflow_exporter.py) that accepts a number of arguments to collect information about a device and exports the collected metrics to a prometheus endpoint
- [Dashboard for Grafana](https://grafana.com/grafana/dashboards/17812-ecoflow/)
- [Dashboard for Grafana](docker-compose/grafana/dashboards/ecoflow.json) — bundled in the repo, auto-provisioned via docker compose, includes Extra Battery panels
- [Docker image](https://github.com/berezhinskiy/ecoflow_exporter/pkgs/container/ecoflow_exporter) for your convenience
- [Quick Start guide](docker-compose/) for your pleasure

Expand Down Expand Up @@ -70,28 +70,34 @@ Please, create an issue to let me know if exporter works well (or not) with your

Required:

`DEVICE_SN` - the device serial number
`DEVICE_SN` - the device serial number(s). For multiple devices, use comma-separated values: `DEVICE_SN=SN1,SN2`

`ECOFLOW_USERNAME` - EcoFlow account username

`ECOFLOW_PASSWORD` - EcoFlow account password

Optional:

`DEVICE_NAME` - If given, this name will be exported as `device` label instead of the device serial number
`DEVICE_NAME` - If given, this name will be exported as `device` label instead of the device serial number. For multiple devices, use comma-separated values in the same order as `DEVICE_SN`: `DEVICE_NAME=name1,name2`

`ECOFLOW_API_HOST` - (default: `api.ecoflow.com`).

`EXPORTER_PORT` - (default: `9090`)

`LOG_LEVEL` - (default: `INFO`) Possible values: `DEBUG`, `INFO`, `WARNING`, `ERROR`

- Example of running docker image:
- Example of running docker image with a single device:

```bash
docker run -e DEVICE_SN=<your device SN> -e ECOFLOW_USERNAME=<your username> -e ECOFLOW_PASSWORD=<your password> -it -p 9090:9090 --network=host ghcr.io/berezhinskiy/ecoflow_exporter
```

- Example with multiple devices:

```bash
docker run -e DEVICE_SN=<SN1>,<SN2> -e DEVICE_NAME=<name1>,<name2> -e ECOFLOW_USERNAME=<your username> -e ECOFLOW_PASSWORD=<your password> -it -p 9090:9090 --network=host ghcr.io/berezhinskiy/ecoflow_exporter
```

will run the image with the exporter on `*:9090`

## Quick Start
Expand Down
117 changes: 98 additions & 19 deletions docker-compose/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,13 @@ Project structure:
│ │ └── telegram.tmpl
│ └── alertmanager.yml
├── grafana
│ ├── dashboards
│ │ └── ecoflow.json
│ ├── dashboard.yml
│ └── datasource.yml
├── nginx
│ ├── nginx.conf
│ └── nginx.ssl.conf.example
├── prometheus
│ ├── alerts
│ │ └── ecoflow.yml
Expand All @@ -33,13 +39,13 @@ services:
image: prom/prometheus
...
ports:
- 9090:9090
- 127.0.0.1:9090:9090

alertmanager:
image: prom/alertmanager
...
ports:
- 9093:9093
- 127.0.0.1:9093:9093

grafana:
image: grafana/grafana
Expand All @@ -48,45 +54,67 @@ services:
- 3000:3000

ecoflow_exporter:
image: ghcr.io/berezhinskiy/ecoflow_exporter
build: ..
...
ports:
- 9091:9091
- 127.0.0.1:9091:9091

nginx:
image: nginx:alpine
profiles: [nginx]
...
ports:
- 80:80
- 443:443
```

The compose file defines a stack with four services:
The compose file defines a stack with four services plus an optional `nginx` reverse proxy (enabled via `--profile nginx`):

- `prometheus`
- `alertmanager`
- `grafana`
- `ecoflow_exporter`

When deploying the stack, docker compose maps the default ports for each service to the equivalent ports on the host in order to more easily inspect the web interface of each service.
Prometheus, Alertmanager and ecoflow_exporter ports are bound to `127.0.0.1` — accessible locally or via SSH tunnel only.

## Deploy with docker compose

⚠️ Make sure the ports `9090`, `9091`, `9093` and `3000` on the host are not already in use.
⚠️ Make sure the port `3000` on the host is not already in use (or `80`/`443` when using nginx).

To run all the services together, do the following:

- Create `.env` file inside `docker-compose` folder:

```bash
# Serial number of your device shown in the mobile application
# Serial number(s) of your device(s) shown in the mobile application
# For multiple devices, use comma-separated values
DEVICE_SN="DEVICE_SN"
# Optional: custom device name(s) for Prometheus labels (comma-separated, same order as DEVICE_SN)
DEVICE_NAME="DEVICE_NAME"
# Email entered in the mobile application
ECOFLOW_USERNAME="ECOFLOW_USERNAME"
# Password entereed in the mobile application
# Password entered in the mobile application
ECOFLOW_PASSWORD="ECOFLOW_PASSWORD"
# Username for Grafana Web interface
GRAFANA_USERNAME="admin"
# Password for Grafana Web interface
GRAFANA_PASSWORD="grafana"
# Telegram bot token and chat ID for Alertmanager notifications
TELEGRAM_BOT_TOKEN="TELEGRAM_BOT_TOKEN"
TELEGRAM_CHAT_ID="TELEGRAM_CHAT_ID"

# Example for multiple devices:
# DEVICE_SN="DAEBX1234567,DELTA2ABCDEF"
# DEVICE_NAME="delta-pro,delta-2-max"
```

- Replace `<TELEGRAM_BOT_TOKEN>` and `<TELEGRAM_CHAT_ID>` with your values in [alertmanager.yaml](alertmanager/alertmanager.yml#L39-L40)
- Generate `alertmanager.yml` from the template (uses values from `.env`):

> If you don't want to receive notifications to Telegram, comment out `alertmanager` section in [compose.yaml](compose.yaml#L14-L23) and `alerting` section in [prometheus.yml](prometheus/prometheus.yml#L7-L12)
```bash
export $(grep -v '^#' .env | xargs) && envsubst < alertmanager/alertmanager.yml.example > alertmanager/alertmanager.yml
```

> If you don't want to receive notifications to Telegram, comment out the `alertmanager` section in [compose.yaml](compose.yaml) and the `alerting` section in [prometheus.yml](prometheus/prometheus.yml)

- Change directory to `docker-compose`, then create and start the containers:

Expand All @@ -109,19 +137,70 @@ Listing containers must show four containers running and the port mapping as bel

```bash
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6e300b56ad58 prom/prometheus "/bin/prometheus --c…" About a minute ago Up 59 seconds 0.0.0.0:9090->9090/tcp, :::9090->9090/tcp prometheus
3a13d5b37398 prom/alertmanager "/bin/alertmanager -…" About a minute ago Up 59 seconds 0.0.0.0:9093->9093/tcp, :::9093->9093/tcp alertmanager
de22630b4d3a ghcr.io/berezhinskiy/ecoflow_exporter "python /ecoflow_exp…" About a minute ago Up 59 seconds 0.0.0.0:9091->9091/tcp, :::9091->9091/tcp ecoflow_exporter
1d61e570968d grafana/grafana "/run.sh" About a minute ago Up 59 seconds 0.0.0.0:3000->3000/tcp, :::3000->3000/tcp grafana
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6e300b56ad58 prom/prometheus "/bin/prometheus --c…" 1 minute ago Up 59 seconds 127.0.0.1:9090->9090/tcp prometheus
3a13d5b37398 prom/alertmanager "/bin/alertmanager -…" 1 minute ago Up 59 seconds 127.0.0.1:9093->9093/tcp alertmanager
de22630b4d3a docker-compose-ecoflow… "python /ecoflow_exp…" 1 minute ago Up 59 seconds 127.0.0.1:9091->9091/tcp ecoflow_exporter
1d61e570968d grafana/grafana "/run.sh" 1 minute ago Up 59 seconds 0.0.0.0:3000->3000/tcp grafana

```

## Import Grafana dasboard
## Grafana dashboard

The EcoFlow dashboard is provisioned automatically — navigate to [http://localhost:3000](http://localhost:3000) and use `GRAFANA_USERNAME` / `GRAFANA_PASSWORD` credentials from `.env` file to access Grafana.

## Production deployment

Navigate to [http://localhost:3000](http://localhost:3000) in your web browser and use `GRAFANA_USERNAME` / `GRAFANA_PASSWORD` credentials from `.env` file to access Grafana. It is already configured with prometheus as the default datasource.
> ⚠️ In production, Prometheus (9090), Alertmanager (9093) and ecoflow_exporter (9091) are only accessible from `127.0.0.1` (SSH tunnel). Only Grafana and nginx are exposed publicly.

### Option 1: Direct IP access (no domain)

Open port 3000 in your firewall and access Grafana at `http://<server-ip>:3000`.

No extra configuration needed.

### Option 2: Domain with Cloudflare

Cloudflare handles SSL — nginx serves plain HTTP on port 80.

Add to `.env`:
```bash
GRAFANA_ROOT_URL=https://grafana.yourdomain.com
```

Navigate to Dashboards → Import dashboard → import ID `17812`, select the only existing Prometheus datasource.
Start with nginx profile:
```bash
docker compose --profile nginx up -d
```

In Cloudflare: create an A record pointing to your server IP, enable the orange cloud (proxy).

### Option 3: Domain with Let's Encrypt

1. Obtain a certificate (run once, before starting nginx):
```bash
docker run --rm -p 80:80 \
-v $(pwd)/nginx/certs:/etc/letsencrypt \
certbot/certbot certonly --standalone -d grafana.yourdomain.com
# Certs will be at nginx/certs/live/grafana.yourdomain.com/
```

2. Copy the SSL config and update paths:
```bash
cp nginx/nginx.ssl.conf.example nginx/nginx.conf
# Edit nginx.conf: replace your.domain.com with your actual domain
# Update ssl_certificate paths to /etc/nginx/certs/live/grafana.yourdomain.com/fullchain.pem
```

3. Add to `.env`:
```bash
GRAFANA_ROOT_URL=https://grafana.yourdomain.com
```

4. Start with nginx profile:
```bash
docker compose --profile nginx up -d
```

## Troubleshooting

Expand Down
43 changes: 43 additions & 0 deletions docker-compose/alertmanager/alertmanager.yml.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
route:
# When a new group of alerts is created by an incoming alert, wait at
# least 'group_wait' to send the initial notification.
# This way ensures that you get multiple alerts for the same group that start
# firing shortly after another are batched together on the first
# notification.
group_wait: 10s

# When the first notification was sent, wait 'group_interval' to send a batch
# of new alerts that started firing for that group.
group_interval: 30s

# If an alert has successfully been sent, wait 'repeat_interval' to
# resend them.
repeat_interval: 12h

group_by:
- alertname
- alertstate
- device

receiver: telegram

# All the above attributes are inherited by all child routes and can
# overwritten on each.
routes:
- receiver: telegram
group_wait: 5s
match_re:
severity: critial|warning
continue: true

templates:
- /etc/alertmanager/templates/*.tmpl

receivers:
- name: telegram
telegram_configs:
- bot_token: ${TELEGRAM_BOT_TOKEN}
chat_id: ${TELEGRAM_CHAT_ID}
api_url: https://api.telegram.org
message: '{{ template "telegram.template" . }}'
parse_mode: MarkdownV2
31 changes: 25 additions & 6 deletions docker-compose/compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ services:
container_name: prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.retention.time=90d'
ports:
- 9090:9090
- 127.0.0.1:9090:9090
restart: unless-stopped
volumes:
- ./prometheus:/etc/prometheus
Expand All @@ -17,7 +18,7 @@ services:
command:
- '--config.file=/etc/alertmanager/alertmanager.yml'
ports:
- 9093:9093
- 127.0.0.1:9093:9093
restart: unless-stopped
volumes:
- ./alertmanager:/etc/alertmanager
Expand All @@ -31,22 +32,40 @@ services:
environment:
GF_SECURITY_ADMIN_USER: ${GRAFANA_USERNAME}
GF_SECURITY_ADMIN_PASSWORD: "${GRAFANA_PASSWORD}"
GF_SERVER_ROOT_URL: "${GRAFANA_ROOT_URL:-http://localhost:3000}"
volumes:
- ./grafana:/etc/grafana/provisioning/datasources
- ./grafana/datasource.yml:/etc/grafana/provisioning/datasources/datasource.yml
- ./grafana/dashboard.yml:/etc/grafana/provisioning/dashboards/dashboard.yml
- ./grafana/dashboards:/var/lib/grafana/dashboards
- grafana_data:/var/lib/grafana

ecoflow_exporter:
image: ghcr.io/berezhinskiy/ecoflow_exporter
image: ghcr.io/serhiioliinyk/ecoflow_exporter
container_name: ecoflow_exporter
ports:
- 9091:9091
- 127.0.0.1:9091:9091
restart: unless-stopped
environment:
DEVICE_SN: ${DEVICE_SN}
DEVICE_NAME: ${DEVICE_NAME}
ECOFLOW_USERNAME: ${ECOFLOW_USERNAME}
ECOFLOW_PASSWORD: "${ECOFLOW_PASSWORD}"
EXPORTER_PORT: 9091

nginx:
image: nginx:alpine
container_name: nginx
profiles: [nginx]
ports:
- 80:80
- 443:443
volumes:
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ./nginx/certs:/etc/nginx/certs:ro
depends_on:
- grafana
restart: unless-stopped

volumes:
prom_data:
grafana_data:
grafana_data:
8 changes: 8 additions & 0 deletions docker-compose/grafana/dashboard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
apiVersion: 1

providers:
- name: EcoFlow
type: file
options:
path: /var/lib/grafana/dashboards
foldersFromFilesStructure: false
Loading