diff --git a/.gitignore b/.gitignore index b6e4761..07762d1 100644 --- a/.gitignore +++ b/.gitignore @@ -103,6 +103,7 @@ celerybeat.pid # Environments .env +docker-compose/alertmanager/alertmanager.yml .venv env/ venv/ diff --git a/Dockerfile b/Dockerfile index fdd054d..f9ecc88 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM python:3.12-alpine LABEL org.opencontainers.image.authors="Yaroslav Berezhinskiy " 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 diff --git a/README.md b/README.md index b916768..5de9e9b 100644 --- a/README.md +++ b/README.md @@ -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 @@ -70,7 +70,7 @@ 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 @@ -78,7 +78,7 @@ Required: 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`). @@ -86,12 +86,18 @@ Optional: `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= -e ECOFLOW_USERNAME= -e ECOFLOW_PASSWORD= -it -p 9090:9090 --network=host ghcr.io/berezhinskiy/ecoflow_exporter ``` +- Example with multiple devices: + +```bash +docker run -e DEVICE_SN=, -e DEVICE_NAME=, -e ECOFLOW_USERNAME= -e ECOFLOW_PASSWORD= -it -p 9090:9090 --network=host ghcr.io/berezhinskiy/ecoflow_exporter +``` + will run the image with the exporter on `*:9090` ## Quick Start diff --git a/docker-compose/README.md b/docker-compose/README.md index 6860043..5ba62ac 100644 --- a/docker-compose/README.md +++ b/docker-compose/README.md @@ -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 @@ -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 @@ -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 `` and `` 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: @@ -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://: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 diff --git a/docker-compose/alertmanager/alertmanager.yml.example b/docker-compose/alertmanager/alertmanager.yml.example new file mode 100644 index 0000000..743ae22 --- /dev/null +++ b/docker-compose/alertmanager/alertmanager.yml.example @@ -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 diff --git a/docker-compose/compose.yaml b/docker-compose/compose.yaml index 29333c8..b6b9d9a 100644 --- a/docker-compose/compose.yaml +++ b/docker-compose/compose.yaml @@ -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 @@ -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 @@ -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: \ No newline at end of file diff --git a/docker-compose/grafana/dashboard.yml b/docker-compose/grafana/dashboard.yml new file mode 100644 index 0000000..4b0603e --- /dev/null +++ b/docker-compose/grafana/dashboard.yml @@ -0,0 +1,8 @@ +apiVersion: 1 + +providers: +- name: EcoFlow + type: file + options: + path: /var/lib/grafana/dashboards + foldersFromFilesStructure: false \ No newline at end of file diff --git a/docker-compose/grafana/dashboards/ecoflow.json b/docker-compose/grafana/dashboards/ecoflow.json new file mode 100644 index 0000000..39916e8 --- /dev/null +++ b/docker-compose/grafana/dashboards/ecoflow.json @@ -0,0 +1,4201 @@ +{ + "__elements": {}, + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "description": "EcoFlow dashboard", + "editable": true, + "fiscalYearStartMonth": 0, + "gnetId": 17812, + "graphTooltip": 2, + "id": null, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [ + { + "options": { + "0": { + "color": "light-red", + "index": 1, + "text": "Offline" + }, + "1": { + "color": "green", + "index": 0, + "text": "Online" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "#f7f7f7", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 4, + "x": 0, + "y": 0 + }, + "id": 46, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value" + }, + "pluginVersion": "9.3.6", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "exemplar": false, + "expr": "ecoflow_online{device=\"$device\"}", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "max": 2500, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "#EAB839", + "value": 1800 + }, + { + "color": "red", + "value": 2400 + } + ] + }, + "unit": "watt" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 2, + "x": 4, + "y": 0 + }, + "id": 23, + "options": { + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "pluginVersion": "9.3.6", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "exemplar": false, + "expr": "ecoflow_inv_input_watts{device=\"$device\"}", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Inverter IN Watts", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "dark-red", + "value": null + }, + { + "color": "#EAB839", + "value": 190 + }, + { + "color": "green", + "value": 210 + } + ] + }, + "unit": "mvolt" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 2, + "x": 6, + "y": 0 + }, + "id": 26, + "options": { + "colorMode": "none", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.6", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "ecoflow_inv_ac_in_vol{device=\"$device\"} or ecoflow_inv_inv_in_vol{device=\"$device\"}", + "instant": false, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Inverter IN Volts", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "options": { + "0": { + "color": "red", + "index": 0 + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "mamp" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 2, + "x": 8, + "y": 0 + }, + "id": 29, + "options": { + "colorMode": "none", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.6", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "ecoflow_inv_ac_in_amp{device=\"$device\"} or ecoflow_inv_inv_in_amp{device=\"$device\"}", + "instant": false, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Inverter IN Current", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [ + { + "options": { + "from": 5500, + "result": { + "color": "green", + "index": 0, + "text": "\u221e" + }, + "to": 6000 + }, + "type": "range" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "yellow", + "value": null + } + ] + }, + "unit": "m" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 10, + "y": 0 + }, + "id": 33, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.6", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "ecoflow_ems_chg_remain_time{device=\"$device\"} or ecoflow_bms_ems_status_chg_remain_time{device=\"$device\"}", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Charging Remaining Time", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "value": null + } + ] + }, + "unit": "mamph" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 2, + "x": 14, + "y": 0 + }, + "id": 37, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.6", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "ecoflow_bms_master_full_cap{device=\"$device\"} or ecoflow_bms_bms_status_full_cap{device=\"$device\"}", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Full Capacity", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "#f7f7f7", + "value": null + } + ] + }, + "unit": "mamp" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 2, + "x": 16, + "y": 0 + }, + "id": 31, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.6", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "abs(ecoflow_bms_master_amp{device=\"$device\"}) or abs(ecoflow_bms_bms_status_amp{device=\"$device\"})", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "BMS Current", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "mvolt" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 2, + "x": 18, + "y": 0 + }, + "id": 28, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.6", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "ecoflow_bms_master_vol{device=\"$device\"} or ecoflow_bms_bms_status_vol{device=\"$device\"}", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Battery Volts", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 3, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "mvolt" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 2, + "x": 20, + "y": 0 + }, + "id": 35, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.6", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "ecoflow_bms_master_min_cell_vol{device=\"$device\"} or ecoflow_bms_bms_status_min_cell_vol{device=\"$device\"}", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "MIN Cell Volts", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 3, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "mvolt" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 2, + "x": 22, + "y": 0 + }, + "id": 36, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.6", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "ecoflow_bms_master_max_cell_vol{device=\"$device\"} or ecoflow_bms_bms_status_max_cell_vol{device=\"$device\"}", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "MAX Cell Volts", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "yellow", + "value": 10 + }, + { + "color": "green", + "value": 30 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 0, + "y": 2 + }, + "id": 21, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "text": {}, + "textMode": "value" + }, + "pluginVersion": "9.3.6", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "ecoflow_bms_master_f32_show_soc{device=\"$device\"} or ecoflow_bms_bms_status_f32_show_soc{device=\"$device\"} or ecoflow_bms_master_soc{device=\"$device\"}", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "max": 2500, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "#EAB839", + "value": 1800 + }, + { + "color": "red", + "value": 2400 + } + ] + }, + "unit": "watt" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 2, + "x": 4, + "y": 4 + }, + "id": 24, + "options": { + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "pluginVersion": "9.3.6", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "exemplar": false, + "expr": "ecoflow_inv_output_watts{device=\"$device\"}", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Inverter OUT Watts", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "dark-red", + "value": null + }, + { + "color": "#EAB839", + "value": 190 + }, + { + "color": "green", + "value": 210 + } + ] + }, + "unit": "mvolt" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 2, + "x": 6, + "y": 4 + }, + "id": 27, + "options": { + "colorMode": "none", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.6", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "exemplar": true, + "expr": "ecoflow_inv_inv_out_vol{device=\"$device\"}", + "instant": false, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Inverter OUT Volts", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "options": { + "0": { + "color": "red", + "index": 0 + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "mamp" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 2, + "x": 8, + "y": 4 + }, + "id": 30, + "options": { + "colorMode": "none", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.6", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "exemplar": true, + "expr": "ecoflow_inv_inv_out_amp{device=\"$device\"}", + "instant": false, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Inverter OUT Current", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [ + { + "options": { + "from": 5500, + "result": { + "color": "green", + "index": 0, + "text": "\u221e" + }, + "to": 6000 + }, + "type": "range" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "#EAB839", + "value": 10 + } + ] + }, + "unit": "m" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 10, + "y": 4 + }, + "id": 32, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.6", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "ecoflow_ems_dsg_remain_time{device=\"$device\"} or ecoflow_bms_ems_status_dsg_remain_time{device=\"$device\"}", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Discharging Remaining Time", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "value": null + } + ] + }, + "unit": "mamph" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 2, + "x": 14, + "y": 4 + }, + "id": 34, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.6", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "ecoflow_bms_master_remain_cap{device=\"$device\"} or ecoflow_bms_bms_status_remain_cap{device=\"$device\"}", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Remain Capacity", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "#f7f7f7", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 2, + "x": 16, + "y": 4 + }, + "id": 42, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.6", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "ecoflow_bms_master_cycles{device=\"$device\"} or ecoflow_bms_bms_status_cycles{device=\"$device\"}", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Battery Cycles", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "#EAB839", + "value": 60 + }, + { + "color": "red", + "value": 65 + } + ] + }, + "unit": "celsius" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 2, + "x": 18, + "y": 4 + }, + "id": 38, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.6", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "ecoflow_inv_out_temp{device=\"$device\"}", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Inverter Temp", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "#EAB839", + "value": 40 + }, + { + "color": "red", + "value": 50 + } + ] + }, + "unit": "celsius" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 2, + "x": 20, + "y": 4 + }, + "id": 40, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.6", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "ecoflow_bms_master_min_cell_temp{device=\"$device\"} or ecoflow_bms_bms_status_min_cell_temp{device=\"$device\"}", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "MIN Cell Temp", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "#EAB839", + "value": 40 + }, + { + "color": "red", + "value": 50 + } + ] + }, + "unit": "celsius" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 2, + "x": 22, + "y": 4 + }, + "id": 39, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.6", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "ecoflow_bms_master_max_cell_temp{device=\"$device\"} or ecoflow_bms_bms_status_max_cell_temp{device=\"$device\"}", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "MAX Cell Temp", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [ + { + "options": { + "0": { + "color": "yellow", + "index": 0, + "text": "Standby" + } + }, + "type": "value" + }, + { + "options": { + "from": 0, + "result": { + "color": "green", + "index": 1, + "text": "Charging" + }, + "to": 1000000 + }, + "type": "range" + }, + { + "options": { + "from": -1000000, + "result": { + "color": "red", + "index": 2, + "text": "Discharging" + }, + "to": 0 + }, + "type": "range" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "#f7f7f7", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 4, + "x": 0, + "y": 6 + }, + "id": 41, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "text": {}, + "textMode": "value" + }, + "pluginVersion": "9.3.6", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "ecoflow_bms_master_amp{device=\"$device\"} or ecoflow_bms_bms_status_amp{device=\"$device\"}", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisGridShow": false, + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 50, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 8, + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "min", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "ecoflow_bms_master_soc{device=\"$device\"} or ecoflow_bms_bms_status_f32_show_soc{device=\"$device\"}", + "interval": "", + "legendFormat": "Battery Level", + "range": true, + "refId": "A" + } + ], + "title": "Battery Level", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisGridShow": true, + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "bars", + "fillOpacity": 100, + "gradientMode": "scheme", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "watt" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Input Watts" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Output Watts" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "green", + "mode": "fixed" + } + }, + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 4, + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "min", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "exemplar": true, + "expr": "ecoflow_inv_input_watts{device=\"$device\"}", + "instant": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Input Watts", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "exemplar": true, + "expr": "ecoflow_inv_output_watts{device=\"$device\"}", + "hide": false, + "instant": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Output Watts", + "refId": "B" + } + ], + "title": "I/O Watts", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "log": 10, + "type": "log" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "m" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 16 + }, + "id": 10, + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "min", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.4.3", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "abs(ecoflow_ems_chg_remain_time{device=\"$device\"}) or abs(ecoflow_bms_ems_status_chg_remain_time{device=\"$device\"}) ", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Charging Remain Time", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "abs(ecoflow_ems_dsg_remain_time{device=\"$device\"}) or abs(ecoflow_bms_ems_status_dsg_remain_time{device=\"$device\"})", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Discharging Remain Time", + "range": true, + "refId": "D" + } + ], + "title": "Remaining Time", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "mvolt" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 16 + }, + "id": 2, + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "min", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "exemplar": true, + "expr": "ecoflow_inv_ac_in_vol{device=\"$device\"} > 0", + "interval": "", + "legendFormat": "Inverter In", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "exemplar": true, + "expr": "ecoflow_inv_inv_out_vol{device=\"$device\"} > 0", + "hide": false, + "interval": "", + "legendFormat": "Inverter Out", + "refId": "E" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "exemplar": true, + "expr": "ecoflow_bms_bms_status_vol{device=\"$device\"}", + "hide": true, + "interval": "", + "legendFormat": "BMS", + "refId": "B" + } + ], + "title": "I/O Volts", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "celsius" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 24 + }, + "id": 6, + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "min", + "max" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "exemplar": true, + "expr": "ecoflow_inv_out_temp{device=\"$device\"} != 0", + "interval": "", + "legendFormat": "Inverter", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "ecoflow_pd_car_temp{device=\"$device\"} != 0", + "hide": false, + "interval": "", + "legendFormat": "PD Car", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "ecoflow_bms_master_temp{device=\"$device\"} != 0 or ecoflow_bms_bms_status_temp{device=\"$device\"} != 0", + "hide": false, + "interval": "", + "legendFormat": "BMS", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "ecoflow_bms_master_max_cell_temp{device=\"$device\"} != 0 or ecoflow_bms_bms_status_max_cell_temp{device=\"$device\"} != 0", + "hide": false, + "interval": "", + "legendFormat": "Max Cell", + "range": true, + "refId": "D" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "ecoflow_bms_master_min_cell_temp{device=\"$device\"} != 0 or ecoflow_bms_bms_status_min_cell_temp{device=\"$device\"} != 0", + "hide": false, + "interval": "", + "legendFormat": "Min Cell", + "range": true, + "refId": "E" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "ecoflow_bms_master_max_mos_temp{device=\"$device\"} != 0 or ecoflow_bms_bms_status_max_mos_temp{device=\"$device\"} != 0", + "hide": false, + "interval": "", + "legendFormat": "Max MOS", + "range": true, + "refId": "F" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "ecoflow_bms_master_min_mos_temp{device=\"$device\"} != 0 or ecoflow_bms_bms_status_min_mos_temp{device=\"$device\"} != 0", + "hide": false, + "interval": "", + "legendFormat": "Min MOS", + "range": true, + "refId": "G" + } + ], + "title": "Temperature", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "mamp" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 24 + }, + "id": 18, + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "min", + "max" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "exemplar": true, + "expr": "ecoflow_inv_ac_in_amp{device=\"$device\"}", + "interval": "", + "legendFormat": "AC IN Current", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "exemplar": true, + "expr": "ecoflow_inv_inv_out_amp{device=\"$device\"}", + "hide": false, + "interval": "", + "legendFormat": "Inverter OUT Current", + "refId": "E" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "exemplar": true, + "expr": "abs(ecoflow_bms_bms_status_amp{device=\"$device\"})", + "hide": false, + "interval": "", + "legendFormat": "BMS Current", + "refId": "B" + } + ], + "title": "Current", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "watt" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 32 + }, + "id": 44, + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "min", + "max" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "exemplar": true, + "expr": "ecoflow_pd_usb1_watts{device=\"$device\"}", + "interval": "", + "legendFormat": "USB", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "exemplar": true, + "expr": "ecoflow_pd_typec1_watts{device=\"$device\"}", + "hide": false, + "interval": "", + "legendFormat": "TypeC", + "refId": "C" + } + ], + "title": "USB", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "mvolt" + }, + "overrides": [ + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "BMS" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": false, + "viz": true + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 32 + }, + "id": 45, + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "min", + "max" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "ecoflow_bms_master_vol{device=\"$device\"} or ecoflow_bms_bms_status_vol{device=\"$device\"}", + "hide": false, + "interval": "", + "legendFormat": "BMS", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "ecoflow_bms_master_min_cell_vol{device=\"$device\"} or ecoflow_bms_bms_status_min_cell_vol{device=\"$device\"}", + "hide": false, + "interval": "", + "legendFormat": "MIN Cell", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "ecoflow_bms_master_max_cell_vol{device=\"$device\"} or ecoflow_bms_bms_status_max_cell_vol{device=\"$device\"}", + "hide": false, + "interval": "", + "legendFormat": "MAX Cell", + "range": true, + "refId": "B" + } + ], + "title": "Battery Volts", + "type": "timeseries" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 40 + }, + "id": 53, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 50, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "watt" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 41 + }, + "id": 55, + "options": { + "legend": { + "calcs": [ + "max", + "mean", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "ecoflow_mppt_in_watts/10", + "legendFormat": "Watts in", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "ecoflow_mppt_in_vol * ecoflow_mppt_in_amp / 1000", + "hide": false, + "legendFormat": "Watts on panel", + "range": true, + "refId": "C" + } + ], + "title": "Solar power", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 50, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "volt" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 41 + }, + "id": 61, + "options": { + "legend": { + "calcs": [ + "max", + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "ecoflow_mppt_in_vol/10", + "legendFormat": "Volts in", + "range": true, + "refId": "A" + } + ], + "title": "Solar voltage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 50, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "amp" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 49 + }, + "id": 63, + "options": { + "legend": { + "calcs": [ + "max", + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "ecoflow_mppt_in_amp/100", + "legendFormat": "Amp in", + "range": true, + "refId": "A" + } + ], + "title": "Solar current", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": true, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 50, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "amp" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 49 + }, + "id": 65, + "options": { + "legend": { + "calcs": [ + "max", + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "ecoflow_bms_master_amp/1000", + "legendFormat": "Amp in", + "range": true, + "refId": "A" + } + ], + "title": "Battery Managemant System Power Balance ", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "green", + "mode": "fixed" + }, + "decimals": 2, + "mappings": [], + "max": 5000, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "watth" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 57 + }, + "id": 67, + "options": { + "displayMode": "basic", + "minVizHeight": 10, + "minVizWidth": 0, + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showUnfilled": true + }, + "pluginVersion": "9.3.6", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum_over_time(ecoflow_mppt_in_watts[$__range]) / (count_over_time(ecoflow_mppt_in_watts[1m]) * 60) / 10", + "legendFormat": "Battery charged", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum_over_time(ecoflow_mppt_out_watts[$__range]) / (count_over_time(ecoflow_mppt_out_watts[1m]) * 60) / 10", + "hide": false, + "legendFormat": "Collected", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum_over_time(ecoflow_mppt_in_watts[24h]) / (count_over_time(ecoflow_mppt_in_watts[1m]) * 60) / 10", + "hide": false, + "legendFormat": "Battery charged (24h)", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum_over_time(ecoflow_mppt_out_watts[24h]) / (count_over_time(ecoflow_mppt_out_watts[1m]) * 60) / 10", + "hide": false, + "legendFormat": "Collected (24h)", + "range": true, + "refId": "D" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum_over_time(ecoflow_mppt_in_watts[7d]) / (count_over_time(ecoflow_mppt_in_watts[1m]) * 60) / 10", + "hide": false, + "legendFormat": "Battery charged (7d)", + "range": true, + "refId": "E" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum_over_time(ecoflow_mppt_out_watts[7d]) / (count_over_time(ecoflow_mppt_out_watts[1m]) * 60) / 10", + "hide": false, + "legendFormat": "Collected (7d)", + "range": true, + "refId": "F" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum_over_time(ecoflow_mppt_in_watts[30d]) / (count_over_time(ecoflow_mppt_in_watts[1m]) * 60) / 10", + "hide": false, + "legendFormat": "Battery charged (30d)", + "range": true, + "refId": "G" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum_over_time(ecoflow_mppt_out_watts[30d]) / (count_over_time(ecoflow_mppt_out_watts[1m]) * 60) / 10", + "hide": false, + "legendFormat": "Collected (30d)", + "range": true, + "refId": "H" + } + ], + "title": "Solar power", + "type": "bargauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 51, + "gradientMode": "opacity", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": -5000 + }, + { + "color": "#EAB839", + "value": -10 + }, + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "watt" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 57 + }, + "id": 69, + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "ecoflow_bms_master_vol/1000 * ecoflow_bms_master_amp/1000", + "legendFormat": "Master BMS", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "ecoflow_mppt_in_vol * ecoflow_mppt_in_amp / 1000", + "hide": false, + "legendFormat": "Solar", + "range": true, + "refId": "B" + } + ], + "title": "Battery Energy Balance", + "type": "timeseries" + } + ], + "title": "MPPT", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 41 + }, + "id": 111, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "yellow", + "value": 10 + }, + { + "color": "green", + "value": 30 + } + ] + }, + "unit": "percent", + "decimals": 0 + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 0, + "y": 42 + }, + "id": 100, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.6", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "ecoflow_bms_slave_f32_show_soc{device=\"$device\"}", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Extra Battery Level", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "value": null + } + ] + }, + "unit": "mamph", + "decimals": 2 + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 2, + "x": 4, + "y": 42 + }, + "id": 101, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.6", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "ecoflow_bms_slave_full_cap{device=\"$device\"}", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Extra Battery Full Capacity", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "value": null + } + ] + }, + "unit": "mamph", + "decimals": 2 + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 2, + "x": 6, + "y": 42 + }, + "id": 102, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.6", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "ecoflow_bms_slave_remain_cap{device=\"$device\"}", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Extra Battery Remain Capacity", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "mvolt", + "decimals": 2 + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 2, + "x": 8, + "y": 42 + }, + "id": 103, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.6", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "ecoflow_bms_slave_vol{device=\"$device\"}", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Extra Battery Volts", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "#f7f7f7", + "value": null + } + ] + }, + "unit": "mamp", + "decimals": 0 + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 2, + "x": 10, + "y": 42 + }, + "id": 104, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.6", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "abs(ecoflow_bms_slave_amp{device=\"$device\"})", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Extra Battery Current", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "#f7f7f7", + "value": null + } + ] + }, + "unit": "none", + "decimals": 0 + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 2, + "x": 12, + "y": 42 + }, + "id": 105, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.6", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "ecoflow_bms_slave_cycles{device=\"$device\"}", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Extra Battery Cycles", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "yellow", + "value": 50 + }, + { + "color": "green", + "value": 80 + } + ] + }, + "unit": "percent", + "decimals": 0 + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 2, + "x": 14, + "y": 42 + }, + "id": 106, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.6", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "ecoflow_bms_slave_soh{device=\"$device\"}", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Extra Battery SOH", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "#EAB839", + "value": 40 + }, + { + "color": "red", + "value": 50 + } + ] + }, + "unit": "celsius", + "decimals": 0 + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 2, + "x": 16, + "y": 42 + }, + "id": 107, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.6", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "ecoflow_bms_slave_min_cell_temp{device=\"$device\"}", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Extra Battery MIN Cell Temp", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "#EAB839", + "value": 40 + }, + { + "color": "red", + "value": 50 + } + ] + }, + "unit": "celsius", + "decimals": 0 + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 2, + "x": 18, + "y": 42 + }, + "id": 108, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.6", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "ecoflow_bms_slave_max_cell_temp{device=\"$device\"}", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Extra Battery MAX Cell Temp", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "mvolt", + "decimals": 3 + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 2, + "x": 20, + "y": 42 + }, + "id": 109, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.6", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "ecoflow_bms_slave_min_cell_vol{device=\"$device\"}", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Extra Battery MIN Cell Volts", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "mvolt", + "decimals": 3 + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 2, + "x": 22, + "y": 42 + }, + "id": 110, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.3.6", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "ecoflow_bms_slave_max_cell_vol{device=\"$device\"}", + "instant": true, + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "title": "Extra Battery MAX Cell Volts", + "type": "stat" + } + ], + "title": "Extra Battery", + "type": "row" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 42 + }, + "id": 59, + "panels": [], + "title": "MQTT", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "bars", + "fillOpacity": 50, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "m/s" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 43 + }, + "id": 48, + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "min", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "exemplar": true, + "expr": "rate(ecoflow_mqtt_messages_receive_total{device=\"$device\"}[1m])", + "interval": "", + "intervalFactor": 3, + "legendFormat": "MQTT Messages", + "refId": "A" + } + ], + "title": "MQTT Messages Per Second", + "type": "timeseries" + } + ], + "refresh": "30s", + "schemaVersion": 37, + "style": "dark", + "tags": [ + "ecoflow", + "prometheus", + "electricity", + "IoT" + ], + "templating": { + "list": [ + { + "current": {}, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "definition": "label_values(ecoflow_online, device)", + "description": "Serial Number of EcoFlow device", + "hide": 0, + "includeAll": false, + "label": "Device", + "multi": false, + "name": "device", + "options": [], + "query": { + "query": "label_values(ecoflow_online, device)", + "refId": "StandardVariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + } + ] + }, + "time": { + "from": "now-3h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "EcoFlow", + "uid": "KGnlUEp4s", + "version": 6, + "weekStart": "monday" +} \ No newline at end of file diff --git a/docker-compose/grafana/datasource.yml b/docker-compose/grafana/datasource.yml index d7b8286..457675c 100644 --- a/docker-compose/grafana/datasource.yml +++ b/docker-compose/grafana/datasource.yml @@ -3,7 +3,8 @@ apiVersion: 1 datasources: - name: Prometheus type: prometheus - url: http://prometheus:9090 + uid: prometheus + url: http://prometheus:9090 isDefault: true access: proxy editable: true diff --git a/docker-compose/nginx/nginx.conf b/docker-compose/nginx/nginx.conf new file mode 100644 index 0000000..545c649 --- /dev/null +++ b/docker-compose/nginx/nginx.conf @@ -0,0 +1,18 @@ +# Nginx config for Grafana behind Cloudflare (or plain HTTP access). +# For HTTPS with Let's Encrypt, see nginx.ssl.conf.example + +server { + listen 80; + server_name _; + + # Pass real visitor IP from Cloudflare + real_ip_header CF-Connecting-IP; + + location / { + proxy_pass http://grafana:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $http_cf_connecting_ip; + proxy_set_header X-Forwarded-For $http_cf_connecting_ip; + proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; + } +} \ No newline at end of file diff --git a/docker-compose/nginx/nginx.ssl.conf.example b/docker-compose/nginx/nginx.ssl.conf.example new file mode 100644 index 0000000..93be5b3 --- /dev/null +++ b/docker-compose/nginx/nginx.ssl.conf.example @@ -0,0 +1,31 @@ +# Nginx config for Grafana with Let's Encrypt SSL. +# 1. Obtain a certificate first: +# docker run --rm -p 80:80 certbot/certbot certonly --standalone -d your.domain.com +# 2. Copy certs to ./nginx/certs/: +# cp /etc/letsencrypt/live/your.domain.com/fullchain.pem ./nginx/certs/ +# cp /etc/letsencrypt/live/your.domain.com/privkey.pem ./nginx/certs/ +# 3. Replace nginx.conf with this file (rename to nginx.conf) + +server { + listen 80; + server_name your.domain.com; + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl; + server_name your.domain.com; + + ssl_certificate /etc/nginx/certs/fullchain.pem; + ssl_certificate_key /etc/nginx/certs/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + + location / { + proxy_pass http://grafana:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + } +} diff --git a/ecoflow_exporter.py b/ecoflow_exporter.py index 9afd2aa..3dde0a7 100644 --- a/ecoflow_exporter.py +++ b/ecoflow_exporter.py @@ -8,13 +8,22 @@ import re import base64 import uuid +import threading from queue import Queue from threading import Timer -from multiprocessing import Process + import requests import paho.mqtt.client as mqtt from prometheus_client import start_http_server, REGISTRY, Gauge, Counter +# Shared Prometheus metric registries (thread-safe) +_gauge_registry = {} +_gauge_registry_lock = threading.Lock() + +_online_gauge = None +_mqtt_counter = None +_worker_metrics_lock = threading.Lock() + class RepeatTimer(Timer): def run(self): @@ -35,7 +44,7 @@ def __init__(self, ecoflow_username, ecoflow_password, ecoflow_api_host): self.mqtt_port = 8883 self.mqtt_username = None self.mqtt_password = None - self.mqtt_client_id = None + self.user_id = None self.authorize() def authorize(self): @@ -72,7 +81,7 @@ def authorize(self): self.mqtt_port = int(response["data"]["port"]) self.mqtt_username = response["data"]["certificateAccount"] self.mqtt_password = response["data"]["certificatePassword"] - self.mqtt_client_id = f"ANDROID_{str(uuid.uuid4()).upper()}_{user_id}" + self.user_id = user_id except KeyError as key: raise Exception(f"Failed to extract key {key} from {response}") @@ -98,14 +107,16 @@ def get_json_response(self, request): class EcoflowMQTT(): - def __init__(self, message_queue, device_sn, username, password, addr, port, client_id, timeout_seconds): + def __init__(self, message_queue, device_sn, username, password, addr, port, user_id, timeout_seconds): self.message_queue = message_queue self.addr = addr self.port = port self.username = username self.password = password - self.client_id = client_id + self.client_id = f"ANDROID_{str(uuid.uuid4()).upper()}_{user_id}" + self.device_sn = device_sn self.topic = f"/app/device/property/{device_sn}" + self.topic_get = f"/app/{user_id}/{device_sn}/thing/property/get" self.timeout_seconds = timeout_seconds self.last_message_time = None self.client = None @@ -116,6 +127,10 @@ def __init__(self, message_queue, device_sn, username, password, addr, port, cli self.idle_timer.daemon = True self.idle_timer.start() + self.quota_timer = RepeatTimer(15, self.request_data) + self.quota_timer.daemon = True + self.quota_timer.start() + def connect(self): if self.client: self.client.loop_stop() @@ -135,23 +150,30 @@ def connect(self): def idle_reconnect(self): if self.last_message_time and time.time() - self.last_message_time > self.timeout_seconds: - log.error(f"No messages received for {self.timeout_seconds} seconds. Reconnecting to MQTT") - # We pull the following into a separate process because there are actually quite a few things that can go - # wrong inside the connection code, including it just timing out and never returning. So this gives us a - # measure of safety around reconnection + log.error(f"[{self.device_sn}] No messages received for {self.timeout_seconds} seconds. Reconnecting to MQTT") while True: - connect_process = Process(target=self.connect) - connect_process.start() - connect_process.join(timeout=60) - connect_process.terminate() - if connect_process.exitcode == 0: - log.info("Reconnection successful, continuing") - # Reset last_message_time here to avoid a race condition between idle_reconnect getting called again - # before on_connect() or on_message() are called + connect_thread = threading.Thread(target=self.connect, daemon=True) + connect_thread.start() + connect_thread.join(timeout=60) + if not connect_thread.is_alive(): + log.info(f"[{self.device_sn}] Reconnection successful, continuing") self.last_message_time = None break else: - log.error("Reconnection errored out, or timed out, attempted to reconnect...") + log.error(f"[{self.device_sn}] Reconnection timed out, retrying...") + + def request_data(self): + if self.client and self.client.is_connected(): + payload = json.dumps({ + "from": "Android", + "id": str(int(time.time() * 1000)), + "moduleType": 0, + "operateType": "latestQuotas", + "params": {}, + "version": "1.0" + }) + log.debug(f"[{self.device_sn}] Requesting data via {self.topic_get}") + self.client.publish(self.topic_get, payload) def on_connect(self, client, userdata, flags, reason_code, properties): # Initialize the time of last message at least once upon connection so that other things that rely on that to be @@ -161,6 +183,7 @@ def on_connect(self, client, userdata, flags, reason_code, properties): case "Success": self.client.subscribe(self.topic) log.info(f"Subscribed to MQTT topic {self.topic}") + self.request_data() case "Keep alive timeout": log.error("Failed to connect to MQTT: connection timed out") case "Unsupported protocol version": @@ -193,7 +216,12 @@ def __init__(self, ecoflow_payload_key, device_name): self.ecoflow_payload_key = ecoflow_payload_key self.device_name = device_name self.name = f"ecoflow_{self.convert_ecoflow_key_to_prometheus_name()}" - self.metric = Gauge(self.name, f"value from MQTT object key {ecoflow_payload_key}", labelnames=["device"]) + with _gauge_registry_lock: + if self.name in _gauge_registry: + self.metric = _gauge_registry[self.name] + else: + self.metric = Gauge(self.name, f"value from MQTT object key {ecoflow_payload_key}", labelnames=["device"]) + _gauge_registry[self.name] = self.metric def convert_ecoflow_key_to_prometheus_name(self): # bms_bmsStatus.maxCellTemp -> bms_bms_status_max_cell_temp @@ -218,29 +246,35 @@ def set(self, value): self.metric.labels(device=self.device_name).set(value) def clear(self): - log.debug(f"Clear {self.name}") - self.metric.clear() + log.debug(f"Clear {self.name} for device {self.device_name}") + self.metric.remove(self.device_name) class Worker: def __init__(self, message_queue, device_name, collecting_interval_seconds=10): + global _online_gauge, _mqtt_counter self.message_queue = message_queue self.device_name = device_name self.collecting_interval_seconds = collecting_interval_seconds self.metrics_collector = [] - self.online = Gauge("ecoflow_online", "1 if device is online", labelnames=["device"]) - self.mqtt_messages_receive_total = Counter("ecoflow_mqtt_messages_receive_total", "total MQTT messages", labelnames=["device"]) + with _worker_metrics_lock: + if _online_gauge is None: + _online_gauge = Gauge("ecoflow_online", "1 if device is online", labelnames=["device"]) + if _mqtt_counter is None: + _mqtt_counter = Counter("ecoflow_mqtt_messages_receive_total", "total MQTT messages", labelnames=["device"]) + self.online = _online_gauge + self.mqtt_messages_receive_total = _mqtt_counter def loop(self): time.sleep(self.collecting_interval_seconds) while True: queue_size = self.message_queue.qsize() if queue_size > 0: - log.info(f"Processing {queue_size} event(s) from the message queue") + log.info(f"[{self.device_name}] Processing {queue_size} event(s) from the message queue") self.online.labels(device=self.device_name).set(1) self.mqtt_messages_receive_total.labels(device=self.device_name).inc(queue_size) else: - log.info("Message queue is empty. Assuming that the device is offline") + log.info(f"[{self.device_name}] Message queue is empty. Assuming that the device is offline") self.online.labels(device=self.device_name).set(0) # Clear metrics for NaN (No data) instead of last value for metric in self.metrics_collector: @@ -304,6 +338,25 @@ def signal_handler(signum, frame): sys.exit(0) +def parse_device_list(): + device_sn_raw = os.getenv("DEVICE_SN", "") + device_name_raw = os.getenv("DEVICE_NAME", "") + + sns = [s.strip() for s in device_sn_raw.split(",") if s.strip()] + names = [s.strip() for s in device_name_raw.split(",")] if device_name_raw else [] + + if not sns: + log.error("Please, provide DEVICE_SN environment variable") + sys.exit(1) + + devices = [] + for i, sn in enumerate(sns): + name = names[i] if i < len(names) and names[i] else sn + devices.append({"sn": sn, "name": name}) + + return devices + + def main(): # Register the signal handler for SIGTERM signal.signal(signal.SIGTERM, signal_handler) @@ -328,8 +381,7 @@ def main(): log.basicConfig(stream=sys.stdout, level=log_level, format='%(asctime)s %(levelname)-7s %(message)s') - device_sn = os.getenv("DEVICE_SN") - device_name = os.getenv("DEVICE_NAME") or device_sn + devices = parse_device_list() ecoflow_username = os.getenv("ECOFLOW_USERNAME") ecoflow_password = os.getenv("ECOFLOW_PASSWORD") ecoflow_api_host = os.getenv("ECOFLOW_API_HOST", "api.ecoflow.com") @@ -337,7 +389,7 @@ def main(): collecting_interval_seconds = int(os.getenv("COLLECTING_INTERVAL", "10")) timeout_seconds = int(os.getenv("MQTT_TIMEOUT", "60")) - if (not device_sn or not ecoflow_username or not ecoflow_password): + if not ecoflow_username or not ecoflow_password: log.error("Please, provide all required environment variables: DEVICE_SN, ECOFLOW_USERNAME, ECOFLOW_PASSWORD") sys.exit(1) @@ -347,17 +399,29 @@ def main(): log.error(error) sys.exit(1) - message_queue = Queue() + start_http_server(exporter_port) - EcoflowMQTT(message_queue, device_sn, auth.mqtt_username, auth.mqtt_password, auth.mqtt_url, auth.mqtt_port, auth.mqtt_client_id, timeout_seconds) + worker_threads = [] + for device in devices: + sn = device["sn"] + name = device["name"] + log.info(f"Setting up device: {name} (SN: {sn})") - metrics = Worker(message_queue, device_name, collecting_interval_seconds) + message_queue = Queue() - start_http_server(exporter_port) + EcoflowMQTT(message_queue, sn, auth.mqtt_username, auth.mqtt_password, auth.mqtt_url, auth.mqtt_port, auth.user_id, timeout_seconds) - try: - metrics.loop() + worker = Worker(message_queue, name, collecting_interval_seconds) + t = threading.Thread(target=worker.loop, name=f"worker-{name}", daemon=True) + t.start() + worker_threads.append(t) + + log.info(f"Started monitoring {len(devices)} device(s)") + + try: + while True: + time.sleep(1) except KeyboardInterrupt: log.info("Received KeyboardInterrupt. Exiting...") sys.exit(0)