Skip to content

Commit

Permalink
Allow to define custom value for label device (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
berezhinskiy authored Jan 9, 2023
1 parent 33ef504 commit 8f9facf
Show file tree
Hide file tree
Showing 2 changed files with 32 additions and 24 deletions.
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,20 @@ Exporter collects all metrics names and their values sent by the device to MQTT
will generate the following metrics:

```plain
ecoflow_bms_bms_status_min_cell_temp{device_sn="XXXXXXXXXXXXXXXX"} 25.0
ecoflow_bms_bms_status_max_cell_temp{device_sn="XXXXXXXXXXXXXXXX"} 27.0
ecoflow_bms_ems_status_f32_lcd_show_soc{device_sn="XXXXXXXXXXXXXXXX"} 56.5
ecoflow_inv_ac_in_vol{device_sn="XXXXXXXXXXXXXXXX"} 242182.0
ecoflow_inv_inv_out_vol{device_sn="XXXXXXXXXXXXXXXX"} 244582.0
ecoflow_bms_bms_status_min_cell_temp{device="XXXXXXXXXXXXXXXX"} 25.0
ecoflow_bms_bms_status_max_cell_temp{device="XXXXXXXXXXXXXXXX"} 27.0
ecoflow_bms_ems_status_f32_lcd_show_soc{device="XXXXXXXXXXXXXXXX"} 56.5
ecoflow_inv_ac_in_vol{device="XXXXXXXXXXXXXXXX"} 242182.0
ecoflow_inv_inv_out_vol{device="XXXXXXXXXXXXXXXX"} 244582.0
```

All metrics are prefixed with `ecoflow` and reports label `device_sn` for multiple device support.
All metrics are prefixed with `ecoflow` and reports label `device` for multiple device support (see [Usage](#usage) section)

## Disclaimers

⚠️ 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_exporter.py#L103-L107), 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_exporter.py#L119-L123), 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 the following EcoFlow products:

Expand Down Expand Up @@ -94,6 +94,8 @@ Required:
Optional:
`DEVICE_NAME` - If given, this name will be exported as `device` label instead of the device serial number
`MQTT_BROKER` - (default: `mqtt.ecoflow.com`)
`MQTT_PORT` - (default: `8883`)
Expand Down
40 changes: 23 additions & 17 deletions ecoflow_exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ class EcoflowMetricException(Exception):


class EcoflowMetric:
def __init__(self, ecoflow_payload_key, device_sn):
def __init__(self, ecoflow_payload_key, device_name):
self.ecoflow_payload_key = ecoflow_payload_key
self.device_sn = device_sn
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_sn"])
self.metric = Gauge(self.name, f"value from MQTT object key {ecoflow_payload_key}", labelnames=["device"])

def convert_ecoflow_key_to_prometheus_name(self):
# bms_bmsStatus.maxCellTemp -> bms_bms_status_max_cell_temp
Expand All @@ -38,34 +38,37 @@ def convert_ecoflow_key_to_prometheus_name(self):
return new

def set(self, value):
# According to best practices for naming metrics and labels, the voltage should be in volts and the current in amperes
# WARNING! This will ruin all Prometheus historical data and backward compatibility of Grafana dashboard
# value = value / 1000 if value.endswith("_vol") or value.endswith("_amp") else value
log.debug(f"Set {self.name} = {value}")
self.metric.labels(device_sn=self.device_sn).set(value)
self.metric.labels(device=self.device_name).set(value)

def clear(self):
log.debug(f"Clear {self.name}")
self.metric.clear()


class Worker:
def __init__(self, message_queue, device_sn, collecting_interval_seconds=5):
def __init__(self, message_queue, device_name, collecting_interval_seconds=5):
self.message_queue = message_queue
self.device_sn = device_sn
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_sn"])
self.mqtt_messages_receive_total = Counter("ecoflow_mqtt_messages_receive_total", "total MQTT messages", labelnames=["device_sn"])
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"])

def run_metrics_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")
self.online.labels(device_sn=self.device_sn).set(1)
self.mqtt_messages_receive_total.labels(device_sn=self.device_sn).inc(queue_size)
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")
self.online.labels(device_sn=self.device_sn).set(0)
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:
metric.clear()
Expand Down Expand Up @@ -105,12 +108,13 @@ def process_payload(self, params):
metric = self.get_metric_by_ecoflow_payload_key(ecoflow_payload_key)
if not metric:
try:
metric = EcoflowMetric(ecoflow_payload_key, self.device_sn)
metric = EcoflowMetric(ecoflow_payload_key, self.device_name)
except EcoflowMetricException as error:
log.error(error)
continue
log.info(f"Created new metric from payload key {metric.ecoflow_payload_key} -> {metric.name}")
self.metrics_collector.append(metric)

metric.set(ecoflow_payload_value)

if ecoflow_payload_key == 'inv.acInVol' and ecoflow_payload_value == 0:
Expand Down Expand Up @@ -169,6 +173,7 @@ def on_connect(self, client, userdata, flags, rc):
def on_disconnect(self, client, userdata, rc):
if rc != 0:
log.error(f"Unexpected MQTT disconnection: {rc}. Will auto-reconnect")
time.sleep(5)

def on_message(self, client, userdata, message):
self.message_queue.put(message.payload.decode("utf-8"))
Expand All @@ -192,22 +197,23 @@ def main():
log.basicConfig(stream=sys.stdout, level=log_level, format='%(asctime)s %(levelname)-7s %(message)s')

device_sn = os.getenv("DEVICE_SN")
username = os.getenv("MQTT_USERNAME")
password = os.getenv("MQTT_PASSWORD")
device_name = os.getenv("DEVICE_NAME") or device_sn
mqtt_username = os.getenv("MQTT_USERNAME")
mqtt_password = os.getenv("MQTT_PASSWORD")
broker_addr = os.getenv("MQTT_BROKER", "mqtt.ecoflow.com")
broker_port = int(os.getenv("MQTT_PORT", "8883"))
exporter_port = int(os.getenv("EXPORTER_PORT", "9090"))

if (not device_sn or not username or not password):
if (not device_sn or not mqtt_username or not mqtt_password):
log.error("Please, provide all required environment variables: DEVICE_SN, MQTT_USERNAME, MQTT_PASSWORD")
sys.exit(1)

message_queue = Queue()

ecoflow_mqtt = EcoflowMQTT(message_queue, device_sn, username, password, broker_addr, broker_port)
ecoflow_mqtt = EcoflowMQTT(message_queue, device_sn, mqtt_username, mqtt_password, broker_addr, broker_port)
ecoflow_mqtt.connect()

metrics = Worker(message_queue, device_sn)
metrics = Worker(message_queue, device_name)
start_http_server(exporter_port)
metrics.run_metrics_loop()

Expand Down

0 comments on commit 8f9facf

Please sign in to comment.