Skip to content

Commit

Permalink
Refactoring and small fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
berezhinskiy committed Jan 7, 2023
1 parent 78b4bb6 commit c5ee9eb
Show file tree
Hide file tree
Showing 2 changed files with 30 additions and 26 deletions.
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

An implementation of a Prometheus exporter for [EcoFlow](https://www.ecoflow.com/) products. To receive information from the device, exporter works the same way as the official mobile application by subscribing to EcoFlow MQTT Broker `mqtt.ecoflow.com`

Unlike `REST API` exporters, it is not required to request for `APP_KEY` and `SECRET_KEY` since MQTT credentials can be extracted from `api.ecoflow.com` (see [Usage](#usage) section). Another benefit of such implementation is that it provides much more device information:
Unlike REST API exporters, it is not required to request for `APP_KEY` and `SECRET_KEY` since MQTT credentials can be extracted from `api.ecoflow.com` (see [Usage](#usage) section). Another benefit of such implementation is that it provides much more device information:

[![Dashboard](images/EcoflowMQTT.png?raw=true)](https://grafana.com/grafana/dashboards/17812-ecoflow-mqtt/)

Expand Down Expand Up @@ -43,7 +43,7 @@ All metrics are prefixed with `ecoflow` and reports label `device_sn` for multip

⚠️ This project is in no way connected to EcoFlow company, and is entirely developed as a fun project with no guarantees of anything.

⚠️ Unexpectedly, some values are always zero (like `ecoflow_bms_ems_status_fan_level` and `ecoflow_inv_fan_state`). It is not a bug in the exporter. No need to create an issue. The exporter just converts the MQTT payload to Prometheus format. It implements small hacks like [here](ecoflow_mqtt_prometheus_exporter.py#L99-L103), but in general, values is provided by the device as it is. To dive into received payloads, enable `DEBUG` logging.
⚠️ Unexpectedly, some values are always zero (like `ecoflow_bms_ems_status_fan_level` and `ecoflow_inv_fan_state`). It is not a bug in the exporter. No need to create an issue. The exporter just converts the MQTT payload to Prometheus format. It implements small hacks like [here](ecoflow_mqtt_prometheus_exporter.py#L103-L107), but in general, values is provided by the device as it is. To dive into received payloads, enable `DEBUG` logging.

⚠️ This has only been tested with __DELTA 2__ Please, create an issue to let me know if exporter works well (or not) with your model.

Expand Down Expand Up @@ -77,7 +77,7 @@ Ecoflow password:
}
```
- The program is parameterized via environment variables:
- Exporter is parameterized via environment variables:
Required:
Expand All @@ -97,23 +97,23 @@ Optional:
`LOG_LEVEL` - (default: `INFO`) Possible values: `DEBUG`, `INFO`, `WARNING`, `ERROR`
Example of running docker image:
- Example of running docker image:
```bash
docker run -e DEVICE_SN=<your device SN> -e MQTT_USERNAME=<your MQTT username> -e MQTT_PASSWORD=<your MQTT password> -it -p 9090:9090 --network=host berezhinskiy/ecoflow-mqtt-prometheus-exporter
```
will run the image with the exporter on *:9090
will run the image with the exporter on `*:9090`
## Exported metrics
## Metrics
Exporter internal metrics:
Prepared by exporter itself:
- `ecoflow_online`
- `ecoflow_mqtt_messages_receive_total`
- `ecoflow_mqtt_messages_receive_created`
Current list of payload metrics:
Actual list of payload metrics:
- `ecoflow_pd_input_watts`
- `ecoflow_pd_usb1_watts`
Expand Down
40 changes: 22 additions & 18 deletions ecoflow_mqtt_prometheus_exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,12 @@ def __init__(self, message_queue, device_sn, collecting_interval_seconds=5):
self.message_queue = message_queue
self.device_sn = device_sn
self.collecting_interval_seconds = collecting_interval_seconds
self.metrics = []
self.online = Gauge("online", "1 if device is online", labelnames=["device_sn"], namespace="ecoflow")
self.mqtt_messages_receive_total = Counter("mqtt_messages_receive_total", "total MQTT messages", labelnames=["device_sn"], namespace="ecoflow")
self.metrics_collector = []
self.online = Gauge("ecoflow_online", "1 if device is online", labelnames=["device_sn"])
self.mqtt_messages_receive_total = Counter("ecoflow_mqtt_messages_receive_total", "total MQTT messages", labelnames=["device_sn"])

def run_metrics_loop(self):
time.sleep(self.collecting_interval_seconds)
while True:
queue_size = self.message_queue.qsize()
if queue_size > 0:
Expand All @@ -57,32 +58,35 @@ def run_metrics_loop(self):
log.info("Message queue is empty. Assuming that the device is offline")
self.online.labels(device_sn=self.device_sn).set(0)
# Clear metrics for NaN (No data) instead of last value
for metric in self.metrics:
for metric in self.metrics_collector:
metric.clear()

while not self.message_queue.empty():
message = self.message_queue.get()
if message is None:
payload = self.message_queue.get()
log.debug(f"Recived payload: {payload}")
if payload is None:
continue

try:
message = json.loads(message)
params = message['params']
except Exception:
log.error(f"Cannot parse MQTT message: {message}")
payload = json.loads(payload)
params = payload['params']
except Exception as error:
log.error(f"Failed to parse MQTT payload: {payload} Error: {error}")
continue
log.debug(f"Processing payload: {params}")
self.process_payload(params)

time.sleep(self.collecting_interval_seconds)

def get_metric_by_ecoflow_payload_key(self, ecoflow_payload_key):
for metric in self.metrics:
for metric in self.metrics_collector:
if metric.ecoflow_payload_key == ecoflow_payload_key:
log.debug(f"Found metric {metric.name} linked to {ecoflow_payload_key}")
return metric
log.debug(f"Cannot find metric linked to {ecoflow_payload_key}")
return False

def process_payload(self, params):
log.debug(f"Processing params: {params}")
for ecoflow_payload_key in params.keys():
ecoflow_payload_value = params[ecoflow_payload_key]
if isinstance(ecoflow_payload_value, list):
Expand All @@ -93,14 +97,14 @@ def process_payload(self, params):
if not metric:
metric = EcoflowMetric(ecoflow_payload_key, self.device_sn)
log.info(f"Created new metric from payload key {metric.ecoflow_payload_key} -> {metric.name}")
self.metrics.append(metric)
self.metrics_collector.append(metric)
metric.set(ecoflow_payload_value)

# Set AC current to zero in case of zero voltage
if ecoflow_payload_key == 'inv.acInVol' and ecoflow_payload_value == 0:
ac_voltage = self.get_metric_by_ecoflow_payload_key('inv.acInAmp')
if ac_voltage:
ac_voltage.set(0)
ac_in_current = self.get_metric_by_ecoflow_payload_key('inv.acInAmp')
if ac_in_current:
log.debug("Set AC inverter input current to zero because of zero inverter voltage")
ac_in_current.set(0)


class EcoflowMQTT():
Expand Down Expand Up @@ -158,7 +162,7 @@ def on_message(self, client, userdata, message):


def main():
log_level = str(os.getenv("LOG_LEVEL", "INFO"))
log_level = os.getenv("LOG_LEVEL", "INFO")

match log_level:
case "DEBUG":
Expand Down

0 comments on commit c5ee9eb

Please sign in to comment.