From 9bdaacb011b854c7d563148850cf4aa07a65dba9 Mon Sep 17 00:00:00 2001 From: rostrovsky Date: Thu, 6 Jan 2022 23:18:21 +0000 Subject: [PATCH 01/11] add second device --- config.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/config.yaml b/config.yaml index 2252b31..391d5f3 100644 --- a/config.yaml +++ b/config.yaml @@ -4,4 +4,7 @@ ble: devices: - name: living room sensor model: LYWSD03MMC - address: A4:C1:38:64:49:02 \ No newline at end of file + address: A4:C1:38:64:49:02 + - name: bedroom sensor + model: LYWSD03MMC + address: A4:C1:38:9B:16:5B \ No newline at end of file From db049ffdb4b51e1550c8dd555d6afb563470950a Mon Sep 17 00:00:00 2001 From: rostrovsky Date: Thu, 6 Jan 2022 23:30:25 +0000 Subject: [PATCH 02/11] connect concurrently --- blexy/utils/config.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/blexy/utils/config.py b/blexy/utils/config.py index f39fb99..357570b 100644 --- a/blexy/utils/config.py +++ b/blexy/utils/config.py @@ -1,8 +1,8 @@ -from abc import abstractproperty from pathlib import Path from importlib import import_module from typing import List import yaml +import concurrent.futures from blexy.devices.abstract_device import AbstractDevice @@ -47,5 +47,7 @@ def connected_devices() -> List[AbstractDevice]: @staticmethod def connect_all_devices(): - for d in GlobalConfig._device_objects: - d.connect() + with concurrent.futures.ThreadPoolExecutor(max_workers=8) as executor: + tasks = [executor.submit(d.connect) for d in GlobalConfig._device_objects] + for _ in concurrent.futures.as_completed(tasks): + pass From 1d76b8c66837f5470dc6ac9664e819c15a4ad948 Mon Sep 17 00:00:00 2001 From: rostrovsky Date: Thu, 6 Jan 2022 23:34:50 +0000 Subject: [PATCH 03/11] remove deprecated decorator --- blexy/devices/abstract_device.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/blexy/devices/abstract_device.py b/blexy/devices/abstract_device.py index ef5f230..fafb31e 100644 --- a/blexy/devices/abstract_device.py +++ b/blexy/devices/abstract_device.py @@ -1,5 +1,5 @@ import json -from abc import ABCMeta, abstractmethod, abstractproperty +from abc import ABCMeta, abstractmethod from typing import List from bluepy import btle @@ -21,7 +21,8 @@ def connect(self) -> "AbstractDevice": def handleNotification(self, cHandle, data): pass - @abstractproperty + @abstractmethod + @property def open_metrics(self) -> List[str]: pass From 6704e1a3c9d337a9f7ec42b537f30163d11cdb22 Mon Sep 17 00:00:00 2001 From: rostrovsky Date: Thu, 6 Jan 2022 23:39:52 +0000 Subject: [PATCH 04/11] placeholder for asyncwaitfornotification --- blexy/devices/abstract_device.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/blexy/devices/abstract_device.py b/blexy/devices/abstract_device.py index fafb31e..8d0db15 100644 --- a/blexy/devices/abstract_device.py +++ b/blexy/devices/abstract_device.py @@ -21,6 +21,9 @@ def connect(self) -> "AbstractDevice": def handleNotification(self, cHandle, data): pass + async def asyncWaitForNotifications(self, timeout) -> bool: + raise NotImplementedError("TODO!") + @abstractmethod @property def open_metrics(self) -> List[str]: From 2f5250fc82601b273c5589b8201d171766637a21 Mon Sep 17 00:00:00 2001 From: rostrovsky Date: Sat, 8 Jan 2022 20:51:35 +0000 Subject: [PATCH 05/11] fetch data asynchronously --- blexy/app.py | 11 ++++++++--- blexy/devices/abstract_device.py | 15 +++++++++++---- blexy/utils/concurrency.py | 11 +++++++++++ 3 files changed, 30 insertions(+), 7 deletions(-) create mode 100644 blexy/utils/concurrency.py diff --git a/blexy/app.py b/blexy/app.py index 2b8d5ed..2b66d86 100644 --- a/blexy/app.py +++ b/blexy/app.py @@ -1,3 +1,4 @@ +import asyncio from starlette.applications import Starlette from starlette.responses import JSONResponse, PlainTextResponse from starlette.routing import Route @@ -17,9 +18,13 @@ async def metrics(request): Returns device readouts in OpenMetrics format """ out = [] - for d in GlobalConfig.connected_devices(): - if d.peripheral.waitForNotifications(5): - out.extend(d.open_metrics) + tasks = [d.asyncWaitForNotifications(5) for d in GlobalConfig.connected_devices()] + results = await asyncio.gather(*tasks) + for success, device in results: + if success: + out.extend(device.open_metrics) + out.append("# EOF") + return PlainTextResponse("\n".join(out)) diff --git a/blexy/devices/abstract_device.py b/blexy/devices/abstract_device.py index 8d0db15..fa02549 100644 --- a/blexy/devices/abstract_device.py +++ b/blexy/devices/abstract_device.py @@ -1,8 +1,10 @@ import json from abc import ABCMeta, abstractmethod -from typing import List +from typing import List, Tuple from bluepy import btle +from blexy.utils.concurrency import run_in_executor + class AbstractDevice(btle.DefaultDelegate, metaclass=ABCMeta): def __init__(self, name, address, interface): @@ -21,11 +23,16 @@ def connect(self) -> "AbstractDevice": def handleNotification(self, cHandle, data): pass - async def asyncWaitForNotifications(self, timeout) -> bool: - raise NotImplementedError("TODO!") + @run_in_executor + def __asyncWaitForNotifications(self, timeout) -> Tuple[bool, "AbstractDevice"]: + return self.peripheral.waitForNotifications(timeout), self + + async def asyncWaitForNotifications(self, timeout) -> Tuple[bool, "AbstractDevice"]: + print(f"{self.name} - waiting for notification with timeout {timeout}s") + return await self.__asyncWaitForNotifications(timeout), self - @abstractmethod @property + @abstractmethod def open_metrics(self) -> List[str]: pass diff --git a/blexy/utils/concurrency.py b/blexy/utils/concurrency.py new file mode 100644 index 0000000..57ff55c --- /dev/null +++ b/blexy/utils/concurrency.py @@ -0,0 +1,11 @@ +import functools +import asyncio + + +def run_in_executor(f): + @functools.wraps(f) + def inner(*args, **kwargs): + loop = asyncio.get_running_loop() + return loop.run_in_executor(None, lambda: f(*args, **kwargs)) + + return inner From be4dfb38f43913fd0ac87e8da0a365ccf5df0d4c Mon Sep 17 00:00:00 2001 From: rostrovsky Date: Sat, 8 Jan 2022 20:54:19 +0000 Subject: [PATCH 06/11] remove debug --- blexy/app.py | 1 - 1 file changed, 1 deletion(-) diff --git a/blexy/app.py b/blexy/app.py index 2b66d86..77f54fe 100644 --- a/blexy/app.py +++ b/blexy/app.py @@ -29,7 +29,6 @@ async def metrics(request): app = Starlette( - debug=True, routes=[ Route("/", homepage), Route("/metrics", metrics), From 23cd975899f25dfc8d649c7b874a7e5d79b4a892 Mon Sep 17 00:00:00 2001 From: rostrovsky Date: Sat, 8 Jan 2022 21:52:46 +0000 Subject: [PATCH 07/11] twiggy log --- blexy/devices/LYWSD03MMC.py | 9 ++++----- blexy/devices/abstract_device.py | 7 ++++++- blexy/main.py | 3 +++ blexy/utils/config.py | 14 ++++++++++---- requirements.txt | 3 ++- setup.py | 2 +- 6 files changed, 26 insertions(+), 12 deletions(-) diff --git a/blexy/devices/LYWSD03MMC.py b/blexy/devices/LYWSD03MMC.py index 869ebb3..dec181e 100644 --- a/blexy/devices/LYWSD03MMC.py +++ b/blexy/devices/LYWSD03MMC.py @@ -1,4 +1,3 @@ -from bluepy import btle from typing import List from blexy.devices.abstract_device import AbstractDevice @@ -19,20 +18,20 @@ def connect(self): if self.is_connected: return self - print(f"Connecting {self.name} ({self.model})") + self.log.info("Connecting...") self.peripheral.connect(self.address) self.peripheral.writeCharacteristic( 0x0038, b"\x01\x00", True ) # enable notifications of Temperature, Humidity and Battery voltage self.peripheral.writeCharacteristic(0x0046, b"\xf4\x01\x00", True) self.peripheral.withDelegate(self) - print(f"Connected {self.name} ({self.model})") + self.log.info(f"Connected") self.is_connected = True return self def handleNotification(self, cHandle, data): try: - print(f"{self.name} ({self.model}) : received {data}") + self.log.info(f"received data: {data.hex()}") self.temperature = ( int.from_bytes(data[0:2], byteorder="little", signed=True) / 100 ) @@ -42,7 +41,7 @@ def handleNotification(self, cHandle, data): int(round((self.voltage - 2.1), 2) * 100), 100 ) # 3.1 or above --> 100% 2.1 --> 0 % except Exception as e: - print("e") + self.log.error(e) @property def open_metrics(self) -> List[str]: diff --git a/blexy/devices/abstract_device.py b/blexy/devices/abstract_device.py index fa02549..9885aca 100644 --- a/blexy/devices/abstract_device.py +++ b/blexy/devices/abstract_device.py @@ -2,6 +2,7 @@ from abc import ABCMeta, abstractmethod from typing import List, Tuple from bluepy import btle +from twiggy import log from blexy.utils.concurrency import run_in_executor @@ -15,6 +16,10 @@ def __init__(self, name, address, interface): self.peripheral = btle.Peripheral(deviceAddr=None, iface=self.interface) self.is_connected = False + @property + def log(self): + return log.name(f"{self.name}@{self.address}") + @abstractmethod def connect(self) -> "AbstractDevice": pass @@ -28,7 +33,7 @@ def __asyncWaitForNotifications(self, timeout) -> Tuple[bool, "AbstractDevice"]: return self.peripheral.waitForNotifications(timeout), self async def asyncWaitForNotifications(self, timeout) -> Tuple[bool, "AbstractDevice"]: - print(f"{self.name} - waiting for notification with timeout {timeout}s") + self.log.debug(f"waiting for notification with timeout {timeout}s") return await self.__asyncWaitForNotifications(timeout), self @property diff --git a/blexy/main.py b/blexy/main.py index 3c625cd..d091efc 100644 --- a/blexy/main.py +++ b/blexy/main.py @@ -2,6 +2,9 @@ import uvicorn import blexy.app from blexy.utils.config import GlobalConfig +from twiggy import quick_setup + +quick_setup() @click.command() diff --git a/blexy/utils/config.py b/blexy/utils/config.py index 357570b..ef3f093 100644 --- a/blexy/utils/config.py +++ b/blexy/utils/config.py @@ -1,8 +1,10 @@ +import yaml +import concurrent.futures + from pathlib import Path from importlib import import_module from typing import List -import yaml -import concurrent.futures +from twiggy import log from blexy.devices.abstract_device import AbstractDevice @@ -12,9 +14,11 @@ class GlobalConfig: log_level = None devices = None _device_objects = None + log = log.name("GlobalConfig") @staticmethod def load_from_file(file_path: str) -> "GlobalConfig": + GlobalConfig.log.info(f"Loading config from: {file_path}") fp = Path(file_path) with open(fp, "r") as cf: config = yaml.load(cf, yaml.SafeLoader) @@ -38,8 +42,10 @@ def load_from_file(file_path: str) -> "GlobalConfig": ) GlobalConfig._device_objects.append(device_obj) except Exception as e: - print(f'could not import device "{dev_name}" ({dev_model})') - print(e) + GlobalConfig.log.error( + f'could not import device "{dev_name}" ({dev_model})' + ) + GlobalConfig.log.error(e) @staticmethod def connected_devices() -> List[AbstractDevice]: diff --git a/requirements.txt b/requirements.txt index cd4b415..d41258c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,5 @@ starlette pyyaml bluepy uvicorn -click \ No newline at end of file +click +twiggy \ No newline at end of file diff --git a/setup.py b/setup.py index 305768c..adb87c2 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ license="MIT", packages=find_packages(), include_package_data=True, - install_requires=["click", "starlette", "pyyaml", "uvicorn", "bluepy"], + install_requires=["click", "starlette", "pyyaml", "uvicorn", "bluepy", "twiggy"], python_requires=">=3.6", entry_points={ "console_scripts": [ From ae77fcff6bfc81d88b7f1495590e4cb2977f0e7d Mon Sep 17 00:00:00 2001 From: rostrovsky Date: Sat, 8 Jan 2022 22:36:17 +0000 Subject: [PATCH 08/11] configure twiggy log level --- blexy/main.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/blexy/main.py b/blexy/main.py index d091efc..578f310 100644 --- a/blexy/main.py +++ b/blexy/main.py @@ -2,9 +2,7 @@ import uvicorn import blexy.app from blexy.utils.config import GlobalConfig -from twiggy import quick_setup - -quick_setup() +from twiggy import quick_setup, levels @click.command() @@ -19,6 +17,7 @@ def cli(port, config_file, log_level): GlobalConfig.load_from_file(config_file) app_port = port if port else GlobalConfig.port app_log_level = log_level if log_level else GlobalConfig.log_level + quick_setup(min_level=getattr(levels, app_log_level.upper())) GlobalConfig.connect_all_devices() uvicorn.run(blexy.app.app, host="0.0.0.0", port=app_port, log_level=app_log_level) From 164c5c92f3fd2a4bbda20bf631516eda53241da3 Mon Sep 17 00:00:00 2001 From: rostrovsky Date: Sat, 8 Jan 2022 23:43:39 +0000 Subject: [PATCH 09/11] openmetrics format change - draft --- README.md | 2 + blexy/devices/LYWSD03MMC.py | 39 ++++++++++-- blexy/devices/abstract_device.py | 11 +++- blexy/utils/openmetrics.py | 50 +++++++++++++++ setup.py | 2 +- test/test_openmetrics.py | 102 +++++++++++++++++++++++++++++++ 6 files changed, 198 insertions(+), 8 deletions(-) create mode 100644 blexy/utils/openmetrics.py create mode 100644 test/test_openmetrics.py diff --git a/README.md b/README.md index 108d64a..e90042d 100644 --- a/README.md +++ b/README.md @@ -31,5 +31,7 @@ ble: address: xx:xx:xx:xx:xx:xx ``` +## Output + ## Supported devices * `LYWSD03MMC` (Xiaomi) \ No newline at end of file diff --git a/blexy/devices/LYWSD03MMC.py b/blexy/devices/LYWSD03MMC.py index dec181e..a50a451 100644 --- a/blexy/devices/LYWSD03MMC.py +++ b/blexy/devices/LYWSD03MMC.py @@ -1,5 +1,6 @@ from typing import List from blexy.devices.abstract_device import AbstractDevice +from blexy.utils.openmetrics import OpenMetric, OpenMetricType class LYWSD03MMC(AbstractDevice): @@ -44,11 +45,39 @@ def handleNotification(self, cHandle, data): self.log.error(e) @property - def open_metrics(self) -> List[str]: + def open_metrics(self) -> List[OpenMetric]: output = [ - f"temperature{self._open_metrics_labels} {self.temperature}", - f"humidity{self._open_metrics_labels} {self.humidity}", - f"voltage{self._open_metrics_labels} {self.voltage}", - f"battery_level{self._open_metrics_labels} {self.battery_level}", + # f"temperature_celsius{self._open_metrics_labels} {self.temperature}", + # f"humidity_percent{self._open_metrics_labels} {self.humidity}", + # f"voltage_volt{self._open_metrics_labels} {self.voltage}", + # f"battery_level_percent{self._open_metrics_labels} {self.battery_level}", + OpenMetric( + name="temperature", + type=OpenMetricType.gauge, + unit="celsius", + labels=self._open_metrics_labels, + value=self.temperature, + ), + OpenMetric( + name="humidity", + type=OpenMetricType.gauge, + unit="percentage", + labels=self._open_metrics_labels, + value=self.humidity, + ), + OpenMetric( + name="voltage", + type=OpenMetricType.gauge, + unit="volts", + labels=self._open_metrics_labels, + value=self.voltage, + ), + OpenMetric( + name="battery_level", + type=OpenMetricType.gauge, + unit="percentage", + labels=self._open_metrics_labels, + value=self.battery_level, + ), ] return output diff --git a/blexy/devices/abstract_device.py b/blexy/devices/abstract_device.py index 9885aca..7f74ac0 100644 --- a/blexy/devices/abstract_device.py +++ b/blexy/devices/abstract_device.py @@ -42,8 +42,15 @@ def open_metrics(self) -> List[str]: pass @property - def _open_metrics_labels(self) -> str: - return f'{{name="{self.name}",model="{self.model}",manufacturer="{self.manufacturer}",address="{self.address}",interface="{self.interface}"}}' + def _open_metrics_labels(self) -> dict: + # return f'{{name="{self.name}",model="{self.model}",manufacturer="{self.manufacturer}",address="{self.address}",interface="{self.interface}"}}' + return { + "name": self.name, + "model": self.model, + "manufacturer": self.manufacturer, + "address": self.address, + "interface": self.interface, + } @property def as_dict(self) -> dict: diff --git a/blexy/utils/openmetrics.py b/blexy/utils/openmetrics.py new file mode 100644 index 0000000..14812d4 --- /dev/null +++ b/blexy/utils/openmetrics.py @@ -0,0 +1,50 @@ +from typing import Any, List +from dataclasses import dataclass + + +class OpenMetricType: + gauge = "gauge" + counter = "counter" + state_set = "state_set" + info = "info" + histogram = "histogram" + gauge_histogram = "gauge_histogram" + summary = "summary" + unknown = "unknown" + + +@dataclass +class OpenMetric: + name: str + type: str + unit: str + labels: dict + value: Any + + @property + def labels_text(self): + out = [] + for k, v in self.labels.items(): + out.append(f'{k}="{v}"') + return ",".join(out) + + @property + def text(self): + return f"{self.name}_{self.unit}_{self.type}{{{self.labels_text}}} {self.value}" + + +class OpenMetricsAggregator: + def __init__(self): + self.metrics = [] + + def add_metric(self, metric: OpenMetric) -> "OpenMetricsAggregator": + self.metrics.append(metric) + return self + + def add_metrics(self, metrics: List[OpenMetric]) -> "OpenMetricsAggregator": + self.metrics.extend(metrics) + return self + + @property + def text(self): + return "TODO" diff --git a/setup.py b/setup.py index adb87c2..0e9ddab 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ description="Simple OpenMetrics exporter for BLE devices", long_description=long_description, long_description_content_type="text/markdown", - version="0.1.0", + version="0.2.0", url="https://github.com/rostrovsky/blexy", license="MIT", packages=find_packages(), diff --git a/test/test_openmetrics.py b/test/test_openmetrics.py new file mode 100644 index 0000000..8adcd81 --- /dev/null +++ b/test/test_openmetrics.py @@ -0,0 +1,102 @@ +import pytest +from blexy.utils.openmetrics import OpenMetric, OpenMetricType, OpenMetricsAggregator + + +def test_openmetric_text(): + om = OpenMetric( + name="temperature", + type=OpenMetricType.gauge, + unit="celsius", + labels={"x": 1, "y": "something", "abcde": "hello #!!", "double": 0.113}, + value=22.34, + ) + + assert ( + om.text + == 'temperature_celsius_gauge{x="1",y="something",abcde="hello #!!",double="0.113"} 22.34' + ) + + +def test_openmetrics_aggregator(): + dev1_labels = { + "name": "sensor 1", + "model": "model 1", + "manufacturer": "acme", + "address": "P:Q:R:S:T", + "interface": 0, + } + + dev2_labels = { + "name": "sensor 2", + "model": "model 1", + "manufacturer": "acme", + "address": "A:B:C:D:E", + "interface": 0, + } + + dev1_metrics = [ + OpenMetric( + name="temperature", + type=OpenMetricType.gauge, + unit="celsius", + labels=dev1_labels, + value=22.33, + ), + OpenMetric( + name="humidity", + type=OpenMetricType.gauge, + unit="percentage", + labels=dev1_labels, + value=48, + ), + OpenMetric( + name="voltage", + type=OpenMetricType.gauge, + unit="volts", + labels=dev1_labels, + value=3.11, + ), + OpenMetric( + name="battery_level", + type=OpenMetricType.gauge, + unit="percentage", + labels=dev1_labels, + value=77, + ), + ] + + dev2_metrics = [ + OpenMetric( + name="temperature", + type=OpenMetricType.gauge, + unit="celsius", + labels=dev2_labels, + value=25.11, + ), + OpenMetric( + name="humidity", + type=OpenMetricType.gauge, + unit="percentage", + labels=dev2_labels, + value=51, + ), + OpenMetric( + name="voltage", + type=OpenMetricType.gauge, + unit="volts", + labels=dev2_labels, + value=3.03, + ), + OpenMetric( + name="battery_level", + type=OpenMetricType.gauge, + unit="percentage", + labels=dev2_labels, + value=88, + ), + ] + + oma = OpenMetricsAggregator() + oma.add_metrics(dev1_metrics).add_metrics(dev2_metrics) + + print(f"\n\n{oma.text}") From 62f515835fa4069550f9cc48464488259b8b8be8 Mon Sep 17 00:00:00 2001 From: rostrovsky Date: Sun, 9 Jan 2022 00:11:45 +0000 Subject: [PATCH 10/11] fix openmetrics format --- README.md | 28 ++++++++++++++++++++++++++-- blexy/app.py | 8 ++++---- blexy/devices/LYWSD03MMC.py | 4 ---- blexy/devices/abstract_device.py | 1 - blexy/utils/openmetrics.py | 28 +++++++++++++++++++++++++++- setup.py | 3 +-- 6 files changed, 58 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index e90042d..fafd291 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # blexy -Simple [OpenMetrics](https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md) exporter for BLE devices +Simple [OpenMetrics](https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md) exporter for BLE devices. + +## Requirements +* device with BLE compliant transceiver (e.g. RPi 3+) +* Python 3.7+ ## Running ``` @@ -31,7 +35,27 @@ ble: address: xx:xx:xx:xx:xx:xx ``` -## Output +## Example output +``` +$ curl localhost:8080/metrics +# TYPE battery_level_percentage gauge +# UNIT battery_level_percentage percentage +battery_level_percentage_gauge{name="living room sensor",model="LYWSD03MMC",manufacturer="Xiaomi",address="xx:xx:xx:xx:xx:xx",interface="0"} 77 +battery_level_percentage_gauge{name="bedroom sensor",model="LYWSD03MMC",manufacturer="Xiaomi",address="xx:xx:xx:xx:xx:xx",interface="0"} 88 +# TYPE humidity_percentage gauge +# UNIT humidity_percentage percentage +humidity_percentage_gauge{name="living room sensor",model="LYWSD03MMC",manufacturer="Xiaomi",address="xx:xx:xx:xx:xx:xx",interface="0"} 48 +humidity_percentage_gauge{name="bedroom sensor",model="LYWSD03MMC",manufacturer="Xiaomi",address="xx:xx:xx:xx:xx:xx",interface="0"} 51 +# TYPE temperature_celsius gauge +# UNIT temperature_celsius celsius +temperature_celsius_gauge{name="living room sensor",model="LYWSD03MMC",manufacturer="Xiaomi",address="xx:xx:xx:xx:xx:xx",interface="0"} 22.33 +temperature_celsius_gauge{name="bedroom sensor",model="LYWSD03MMC",manufacturer="Xiaomi",address="xx:xx:xx:xx:xx:xx",interface="0"} 25.11 +# TYPE voltage_volts gauge +# UNIT voltage_volts volts +voltage_volts_gauge{name="living room sensor",model="LYWSD03MMC",manufacturer="Xiaomi",address="xx:xx:xx:xx:xx:xx",interface="0"} 3.11 +voltage_volts_gauge{name="bedroom sensor",model="LYWSD03MMC",manufacturer="Xiaomi",address="xx:xx:xx:xx:xx:xx",interface="0"} 3.03 +# EOF +``` ## Supported devices * `LYWSD03MMC` (Xiaomi) \ No newline at end of file diff --git a/blexy/app.py b/blexy/app.py index 77f54fe..7ce95fe 100644 --- a/blexy/app.py +++ b/blexy/app.py @@ -3,6 +3,7 @@ from starlette.responses import JSONResponse, PlainTextResponse from starlette.routing import Route from blexy.utils.config import GlobalConfig +from blexy.utils.openmetrics import OpenMetricsAggregator async def homepage(request): @@ -17,15 +18,14 @@ async def metrics(request): """ Returns device readouts in OpenMetrics format """ - out = [] + oma = OpenMetricsAggregator() tasks = [d.asyncWaitForNotifications(5) for d in GlobalConfig.connected_devices()] results = await asyncio.gather(*tasks) for success, device in results: if success: - out.extend(device.open_metrics) - out.append("# EOF") + oma.add_metrics(device.open_metrics) - return PlainTextResponse("\n".join(out)) + return PlainTextResponse(oma.text) app = Starlette( diff --git a/blexy/devices/LYWSD03MMC.py b/blexy/devices/LYWSD03MMC.py index a50a451..f8c5d9a 100644 --- a/blexy/devices/LYWSD03MMC.py +++ b/blexy/devices/LYWSD03MMC.py @@ -47,10 +47,6 @@ def handleNotification(self, cHandle, data): @property def open_metrics(self) -> List[OpenMetric]: output = [ - # f"temperature_celsius{self._open_metrics_labels} {self.temperature}", - # f"humidity_percent{self._open_metrics_labels} {self.humidity}", - # f"voltage_volt{self._open_metrics_labels} {self.voltage}", - # f"battery_level_percent{self._open_metrics_labels} {self.battery_level}", OpenMetric( name="temperature", type=OpenMetricType.gauge, diff --git a/blexy/devices/abstract_device.py b/blexy/devices/abstract_device.py index 7f74ac0..5ea1cb3 100644 --- a/blexy/devices/abstract_device.py +++ b/blexy/devices/abstract_device.py @@ -43,7 +43,6 @@ def open_metrics(self) -> List[str]: @property def _open_metrics_labels(self) -> dict: - # return f'{{name="{self.name}",model="{self.model}",manufacturer="{self.manufacturer}",address="{self.address}",interface="{self.interface}"}}' return { "name": self.name, "model": self.model, diff --git a/blexy/utils/openmetrics.py b/blexy/utils/openmetrics.py index 14812d4..5c60a7b 100644 --- a/blexy/utils/openmetrics.py +++ b/blexy/utils/openmetrics.py @@ -1,5 +1,6 @@ from typing import Any, List from dataclasses import dataclass +from itertools import groupby class OpenMetricType: @@ -32,6 +33,14 @@ def labels_text(self): def text(self): return f"{self.name}_{self.unit}_{self.type}{{{self.labels_text}}} {self.value}" + @property + def type_metadata(self): + return f"# TYPE {self.name}_{self.unit} {self.type}" + + @property + def unit_metadata(self): + return f"# UNIT {self.name}_{self.unit} {self.unit}" + class OpenMetricsAggregator: def __init__(self): @@ -45,6 +54,23 @@ def add_metrics(self, metrics: List[OpenMetric]) -> "OpenMetricsAggregator": self.metrics.extend(metrics) return self + @property + def grouped_metrics(self): + sorted_metrics = sorted( + self.metrics, key=lambda m: f"{m.name}_{m.unit}_{m.type}" + ) + return groupby(sorted_metrics, lambda m: m.name) + @property def text(self): - return "TODO" + out = [] + for metric_name, group in self.grouped_metrics: + # out.append(f"{group}: {metric_name}") + for metric in group: + if metric.type_metadata not in out: + out.append(metric.type_metadata) + if metric.unit_metadata not in out: + out.append(metric.unit_metadata) + out.append(str(metric.text)) + out.append("# EOF") + return "\n".join(out) diff --git a/setup.py b/setup.py index 0e9ddab..fc1ecc6 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ packages=find_packages(), include_package_data=True, install_requires=["click", "starlette", "pyyaml", "uvicorn", "bluepy", "twiggy"], - python_requires=">=3.6", + python_requires=">=3.7", entry_points={ "console_scripts": [ "blexy = blexy.main:cli", @@ -26,7 +26,6 @@ "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", From 5b14d6455c7f6c8fa8476a152e2f42bb4924f5aa Mon Sep 17 00:00:00 2001 From: rostrovsky Date: Sun, 9 Jan 2022 00:28:14 +0000 Subject: [PATCH 11/11] exclude test from package --- MANIFEST.in | 1 + setup.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..33e0aad --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +recursive-exclude test * \ No newline at end of file diff --git a/setup.py b/setup.py index fc1ecc6..2f0cefc 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ version="0.2.0", url="https://github.com/rostrovsky/blexy", license="MIT", - packages=find_packages(), + packages=find_packages(exclude=["*test.*","*test"]), include_package_data=True, install_requires=["click", "starlette", "pyyaml", "uvicorn", "bluepy", "twiggy"], python_requires=">=3.7",