Files
lywsd03mmc_exporter/bt_exporter.py

106 lines
3.5 KiB
Python
Raw Normal View History

2023-10-30 17:24:42 +00:00
"""Xiaomi Mi Temperature and Humidity Monitor 2 (LYWSD03MMC) prom exporter"""
import time
import argparse
2023-11-27 19:12:09 +02:00
import logging
2023-10-30 17:24:42 +00:00
from prometheus_client import start_http_server, Gauge, Enum
from bluepy import btle
2023-11-27 19:12:09 +02:00
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%d-%m-%Y %H:%M:%S')
log = logging.getLogger('LYWSD03MMC-exporter')
2023-10-30 17:24:42 +00:00
class XiaoMiTemp(btle.DefaultDelegate):
2023-10-30 22:51:07 +02:00
def __init__(self, mac, location, temp, humid, batt):
2023-10-30 17:24:42 +00:00
btle.DefaultDelegate.__init__(self)
2023-10-30 22:51:07 +02:00
self.mac = mac
self.loc = location
2023-10-30 17:24:42 +00:00
self.temp = temp
self.humid = humid
self.batt = batt
def handleNotification(self, cHandle, data):
2023-10-30 22:51:07 +02:00
databytes = bytearray(data)
self.temp.labels(self.mac, self.loc).set(int.from_bytes(databytes[0:2], "little") / 100)
self.humid.labels(self.mac, self.loc).set(int.from_bytes(databytes[2:3], "little"))
self.batt.labels(self.mac, self.loc).set(int.from_bytes(databytes[3:5], "little") / 1000)
2023-10-30 17:24:42 +00:00
class AppMetrics:
def __init__(self, devices, polling_interval_seconds=180):
self.polling_interval_seconds = polling_interval_seconds
self.devices = devices
2023-11-27 19:12:09 +02:00
self.fetch_count = int(0)
2023-10-30 17:24:42 +00:00
# Metrics scheme
self.temperature = Gauge("lywsd03mmc_temp", "Current temperature", ['mac', 'location'])
self.humidity = Gauge("lywsd03mmc_humid", "Current humidity", ['mac', 'location'])
self.battery = Gauge("lywsd03mmc_batt", "Battery level", ['mac', 'location'])
def run_metrics_loop(self):
"""Metrics fetching loop"""
while True:
self.fetch()
time.sleep(self.polling_interval_seconds)
def fetch(self):
2023-11-27 19:12:09 +02:00
self.fetch_count += 1
log.info(f"Fetch loop started. Iteration {self.fetch_count}")
2023-10-30 17:24:42 +00:00
for mac, location in self.devices:
2023-11-27 19:12:09 +02:00
log.info(f"Fetching '{mac}' ({location})")
2023-10-30 22:51:07 +02:00
p = btle.Peripheral()
2024-05-01 16:01:01 +03:00
p.withDelegate(XiaoMiTemp(mac, location, self.temperature, self.humidity, self.battery))
2023-10-30 17:24:42 +00:00
# BLE performs very poorly, constant errors are not uncommon
for attempt in range(10):
try:
p.connect(mac)
p.waitForNotifications(15.0)
2023-11-27 19:12:09 +02:00
log.info(f"Fetched '{mac}' ({location}). Attempt {attempt}.")
2023-10-30 17:24:42 +00:00
break
except Exception as e:
2023-11-27 19:12:09 +02:00
log.info(f"Failed to fetch '{mac}' ({location}). Attempt {attempt}.")
2023-10-30 17:24:42 +00:00
pass
finally:
p.disconnect()
2023-10-30 22:51:07 +02:00
2023-10-30 17:24:42 +00:00
def main():
parser = argparse.ArgumentParser(
2023-10-30 22:51:07 +02:00
description="Xiaomi Mi Temperature and Humidity Monitor 2 (LYWSD03MMC) prom exporter")
2023-10-30 17:24:42 +00:00
parser.add_argument(
2023-10-30 22:51:07 +02:00
'--device',
action='append',
required=True,
help="BLE Device in 'MAC;location' format")
2023-10-30 17:24:42 +00:00
parser.add_argument(
2023-10-30 22:51:07 +02:00
'--polling-interval',
default=180,
type=int,
help='Polling interval in seconds')
2023-10-30 17:24:42 +00:00
parser.add_argument(
2023-10-30 22:51:07 +02:00
'--port',
default=9877,
type=int,
help='Exporter port')
2023-10-30 17:24:42 +00:00
args = parser.parse_args()
devices = [tuple(device.split(';')) for device in args.device]
polling_interval = args.polling_interval
exporter_port = args.port
app_metrics = AppMetrics(
polling_interval_seconds=polling_interval,
devices=devices
)
start_http_server(exporter_port)
app_metrics.run_metrics_loop()
2023-10-30 22:51:07 +02:00
2023-10-30 17:24:42 +00:00
if __name__ == "__main__":
main()