From 208a82142c7f151cecd88e7331ed683cac911481 Mon Sep 17 00:00:00 2001 From: DasMoorhuhn Date: Sat, 22 Jun 2024 18:05:28 +0200 Subject: [PATCH 01/10] added device recocnition --- .gitignore | 3 ++- python/src/devices.example.yml | 8 ++++++++ python/src/devices.py | 23 +++++++++++++++++++++++ python/src/devices.yml | 8 -------- python/src/discovery.py | 33 ++++++++++++++++++++++----------- python/src/loop.py | 3 +-- python/src/main.py | 20 +++++++------------- 7 files changed, 63 insertions(+), 35 deletions(-) create mode 100644 python/src/devices.example.yml delete mode 100644 python/src/devices.yml diff --git a/.gitignore b/.gitignore index ed8ebf5..8647625 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -__pycache__ \ No newline at end of file +__pycache__ +devices.yml \ No newline at end of file diff --git a/python/src/devices.example.yml b/python/src/devices.example.yml new file mode 100644 index 0000000..30041aa --- /dev/null +++ b/python/src/devices.example.yml @@ -0,0 +1,8 @@ +devices: + - mac: A4:C1:38:00:00:00 + name: "my_device_1" + room: "my_room_1" + + - mac: A4:C1:38:00:00:00 + name: "my_device_2" + room: "my_room_2" diff --git a/python/src/devices.py b/python/src/devices.py index 31e56f0..b5f480c 100644 --- a/python/src/devices.py +++ b/python/src/devices.py @@ -1,2 +1,25 @@ import yaml +import os + +workdir, filename = os.path.split(os.path.abspath(__file__)) +config_file = f"{workdir}{os.sep}devices.yml" + + +class Device: + def __init__(self, data): + self.mac = data['mac'] + self.name = data['name'] + self.room = data['room'] + + +def get_devices(): + devices_list = [] + with open(file=config_file, mode='r') as file: + devices = yaml.safe_load(file) + for device in devices['devices']: devices_list.append(Device(data=device)) + return devices_list + + + + diff --git a/python/src/devices.yml b/python/src/devices.yml deleted file mode 100644 index ce23077..0000000 --- a/python/src/devices.yml +++ /dev/null @@ -1,8 +0,0 @@ -devices: - - "A4:C1:38:83:05:E8": - name: "My Sensor" - room: "My Room" - - - "...": - name: "..." - room: "..." diff --git a/python/src/discovery.py b/python/src/discovery.py index d01ff82..afe4609 100644 --- a/python/src/discovery.py +++ b/python/src/discovery.py @@ -1,10 +1,9 @@ -import asyncio -from threading import Thread from bluepy.btle import DefaultDelegate from bluepy.btle import Scanner from datetime import datetime from data_class import Data +from devices import get_devices as get_device_from_config # This is the list, where the responses will be stored from the `handleDiscovery` devices = [] @@ -21,6 +20,7 @@ class ScanDelegate(DefaultDelegate): if self.is_temperature(sdid, val): data_obj = Data(self.parse_data(val)) if self.is_atc_device(dev, data_obj): + device_from_config = self.get_device(dev) devices.append([dev, data_obj]) @staticmethod @@ -29,13 +29,16 @@ class ScanDelegate(DefaultDelegate): if len(val) != 30: return False return True - @staticmethod - def is_atc_device(dev, data_obj): + def is_atc_device(self, dev, data_obj): global devices if 'A4:C1:38' not in dev.addr.upper(): return False for device in devices: if str(device[0].addr) == str(dev.addr): return False - print("Device %s (%s), RSSI=%d dB" % (dev.addr.upper(), dev.addrType, dev.rssi)) + + device_from_config = self.get_device(dev) + + try:print(f"Device: {dev.addr.upper()} ({dev.addrType}), RSSI: {dev.rssi} dB, Room: {device_from_config.room}") + except:print(f"Device: {dev.addr.upper()} ({dev.addrType}), RSSI: {dev.rssi} dB, Room: ?") print(f'\tTemp: {data_obj.temperature}°C, Humid: {data_obj.humidity}%, Batt: {data_obj.battery_percent}%\n') return True @@ -54,12 +57,20 @@ class ScanDelegate(DefaultDelegate): 'count': bytes[14], } + @staticmethod + def get_device(dev): + return next((d for d in get_device_from_config() if d.mac == dev.addr.upper()), None) -def start_discovery(timeout=10.0): - global devices - print(f'Start discovery with timout {timeout}s...') - scanner = Scanner().withDelegate(ScanDelegate()) - scanner.scan(timeout=timeout, passive=True) +class Discovery: + def __init__(self): + pass - return devices + def start_discovery(self, timeout=20.0): + global devices + print(f'Start discovery with timout {timeout}s...') + + scanner = Scanner().withDelegate(ScanDelegate()) + scanner.scan(timeout=timeout, passive=True) + + return devices diff --git a/python/src/loop.py b/python/src/loop.py index e50ca33..53abbbe 100644 --- a/python/src/loop.py +++ b/python/src/loop.py @@ -1,9 +1,8 @@ from time import sleep -from discovery import start_discovery - def start_loop(interval=60): while True: + from discovery import start_discovery start_discovery() sleep(interval) diff --git a/python/src/main.py b/python/src/main.py index 8f018a1..791c261 100644 --- a/python/src/main.py +++ b/python/src/main.py @@ -1,19 +1,13 @@ -from discovery import start_discovery +from discovery import Discovery from data_class import Data +from loop import start_loop +from time import sleep -devices = start_discovery() +while True: + discovery = Discovery() + devices = discovery.start_discovery() + sleep(10) -if len(devices) > 0: - for device_list in devices: - data = device_list[1] - device = device_list[0] - - data:Data - # print(f'Temp: {data.temperature}°C, Humid: {data.humidity}%, Batt: {data.battery_percent}%') - - -else: - print('No devices found') From 2b1c12f27301eb3027175d08643cc9bf9bcc56d6 Mon Sep 17 00:00:00 2001 From: DasMoorhuhn Date: Sat, 22 Jun 2024 20:46:25 +0200 Subject: [PATCH 02/10] try new data class --- .idea/ATC_Sensor_Gateway.iml | 2 +- .idea/misc.xml | 2 +- python/src/data_class.py | 34 +++++++++++++++++++++++------- python/src/discovery.py | 41 ++++++++++++------------------------ python/src/loop.py | 3 ++- python/src/main.py | 10 ++------- 6 files changed, 46 insertions(+), 46 deletions(-) diff --git a/.idea/ATC_Sensor_Gateway.iml b/.idea/ATC_Sensor_Gateway.iml index f571432..d0876a7 100644 --- a/.idea/ATC_Sensor_Gateway.iml +++ b/.idea/ATC_Sensor_Gateway.iml @@ -2,7 +2,7 @@ - + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 5a5d9ae..12391f3 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,5 +3,5 @@ - + \ No newline at end of file diff --git a/python/src/data_class.py b/python/src/data_class.py index 5a05404..9ddfcf8 100644 --- a/python/src/data_class.py +++ b/python/src/data_class.py @@ -1,12 +1,30 @@ +from datetime import datetime + + class Data: - def __init__(self, data:dict): - self.timestamp = data['timestamp'] - self.mac = data['mac'] - self.temperature = data['temperature'] - self.humidity = data['humidity'] - self.battery_percent = data['battery_percent'] - self.battery_volt = data['battery_volt'] - self.count = data['count'] + def __init__(self, data): + self.timestamp = None + self.mac = None + self.temperature = None + self.humidity = None + self.battery_percent = None + self.battery_volt = None + self.count = None + self.parse_data(data) + + def parse_data(self, val): + data_bytes = [int(val[i:i + 2], 16) for i in range(0, len(val), 2)] + + if data_bytes[8] > 127: + print(data_bytes) + data_bytes[8] -= 256 + self.timestamp = datetime.now().astimezone().replace(microsecond=0).isoformat() + self.mac = ":".join(["{:02X}".format(data_bytes[i]) for i in range(2, 8)]) + self.temperature = (data_bytes[8] * 256 + data_bytes[9]) / 10 + self.humidity = data_bytes[10] + self.battery_percent = data_bytes[11] + self.battery_volt = (data_bytes[12] * 256 + data_bytes[13]) / 1000 + self.count = data_bytes[14] def print_data(self): print(self.to_json()) diff --git a/python/src/discovery.py b/python/src/discovery.py index afe4609..2c965ff 100644 --- a/python/src/discovery.py +++ b/python/src/discovery.py @@ -1,6 +1,5 @@ from bluepy.btle import DefaultDelegate from bluepy.btle import Scanner -from datetime import datetime from data_class import Data from devices import get_devices as get_device_from_config @@ -18,10 +17,11 @@ class ScanDelegate(DefaultDelegate): for (sdid, desc, val) in dev.getScanData(): if self.is_temperature(sdid, val): - data_obj = Data(self.parse_data(val)) + data_obj = Data(val) + if self.is_atc_device(dev, data_obj): device_from_config = self.get_device(dev) - devices.append([dev, data_obj]) + devices.append([dev, data_obj, device_from_config]) @staticmethod def is_temperature(sdid, val): @@ -42,35 +42,22 @@ class ScanDelegate(DefaultDelegate): print(f'\tTemp: {data_obj.temperature}°C, Humid: {data_obj.humidity}%, Batt: {data_obj.battery_percent}%\n') return True - @staticmethod - def parse_data(val): - bytes = [int(val[i:i + 2], 16) for i in range(0, len(val), 2)] - if bytes[8] > 127: - bytes[8] -= 256 - return { - 'timestamp': datetime.now().astimezone().replace(microsecond=0).isoformat(), - 'mac': ":".join(["{:02X}".format(bytes[i]) for i in range(2, 8)]), - 'temperature': (bytes[8] * 256 + bytes[9]) / 10, - 'humidity': bytes[10], - 'battery_percent': bytes[11], - 'battery_volt': (bytes[12] * 256 + bytes[13]) / 1000, - 'count': bytes[14], - } - @staticmethod def get_device(dev): return next((d for d in get_device_from_config() if d.mac == dev.addr.upper()), None) -class Discovery: - def __init__(self): - pass +def cleanup(): + global devices + devices = [] - def start_discovery(self, timeout=20.0): - global devices - print(f'Start discovery with timout {timeout}s...') - scanner = Scanner().withDelegate(ScanDelegate()) - scanner.scan(timeout=timeout, passive=True) +def start_discovery(timeout=20.0): + cleanup() + global devices + print(f'Start discovery with timout {timeout}s...') - return devices + scanner = Scanner().withDelegate(ScanDelegate()) + scanner.scan(timeout=timeout, passive=False) + + return devices diff --git a/python/src/loop.py b/python/src/loop.py index 53abbbe..e50ca33 100644 --- a/python/src/loop.py +++ b/python/src/loop.py @@ -1,8 +1,9 @@ from time import sleep +from discovery import start_discovery + def start_loop(interval=60): while True: - from discovery import start_discovery start_discovery() sleep(interval) diff --git a/python/src/main.py b/python/src/main.py index 791c261..089dbe0 100644 --- a/python/src/main.py +++ b/python/src/main.py @@ -1,13 +1,7 @@ -from discovery import Discovery +from discovery import start_discovery from data_class import Data from loop import start_loop from time import sleep -while True: - discovery = Discovery() - devices = discovery.start_discovery() - sleep(10) - - - +devices = start_discovery() From 961fba65bee565daa7ecc3b0516969f7b2f4b230 Mon Sep 17 00:00:00 2001 From: DasMoorhuhn Date: Sat, 22 Jun 2024 22:37:24 +0200 Subject: [PATCH 03/10] did stuff idk i'm cooked --- Dockerfile | 17 ++++++++++++++--- README.md | 7 ++++--- bluetooth tools/search_for_ble.py | 0 .../search_for_blc.py | 0 bluetooth_tools/search_for_ble.py | 8 ++++++++ log_to_json.py | 13 +++++++++++++ python/requierements.txt | 5 ++++- python/src/data_class.py | 6 ++---- python/src/devices.py | 5 ++--- python/src/discovery.py | 10 +++------- python/src/log_data.py | 1 + python/src/main.py | 4 +--- python/src/mqtt.py | 1 + 13 files changed, 53 insertions(+), 24 deletions(-) delete mode 100644 bluetooth tools/search_for_ble.py rename {bluetooth tools => bluetooth_tools}/search_for_blc.py (100%) create mode 100644 bluetooth_tools/search_for_ble.py create mode 100644 log_to_json.py create mode 100644 python/src/log_data.py create mode 100644 python/src/mqtt.py diff --git a/Dockerfile b/Dockerfile index a474eea..0a9db2a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.12 +FROM python:3.12-alpine3.20 WORKDIR = /src @@ -8,8 +8,19 @@ COPY python/docker_entrypoint.sh / RUN mkdir data -RUN apt-get update && \ - apt-get install -y bluez sudo +# RUN apt-get update && \ +# apt-get install -y bluez sudo + + +RUN apk add --no-cache \ + sudo \ + make \ + bluez \ + bluez-deprecated \ + alsa-utils \ + alsa-utils-doc \ + alsa-lib \ + alsaconf RUN pip3.12 install -r requierements.txt && rm -f requierements.txt diff --git a/README.md b/README.md index dd2f5bf..2675b4a 100644 --- a/README.md +++ b/README.md @@ -5,14 +5,14 @@ Python gateway for the [custom firmware](https://github.com/atc1441/ATC_MiThermo ![](.media/41N1IH9jwoL._AC_SL1024_.jpg) **Features:** -- [DONE] Make in runnable in a docker container (because only cool people are using docker) +- WIP **TODOs:** - [WIP] Can run on Raspberry Pi (3, 4, zero w) or any other Linux driven hardware which has BLE and WiFi support - [WIP] Storing temperature, humidity and battery state as json in a text file - [WIP] Implement a loop for fetching the data every x minute - [WIP] Make discoveries async -- [TODO] Make docker image smaller. I mean shiiit 1GB D: should be possible to be under 500MB +- [WIP] Make docker image smaller. I mean shiiit 1GB D: should be possible to be under 500MB - [TODO] Make a microPython version for using the raspberry pico w or any other microcontroller with BLE and WiFi support - [TODO] Collect data from multiple devices/gateways - [TODO] Command line tool for managing the devices @@ -21,6 +21,7 @@ Python gateway for the [custom firmware](https://github.com/atc1441/ATC_MiThermo - [TODO] MQTT publishing - [TODO] Maybe... a webinterface. But I suck at web stuff, so I don't know. - [TODO] Implement other BLE Sensors +- [BROK] Make in runnable in a docker container (because only cool people are using docker) **Current State** @@ -50,7 +51,7 @@ sudo python3 main.py ### Docker -Build docker container +Build docker container (Currently broken) ```bash docker-compose build ``` diff --git a/bluetooth tools/search_for_ble.py b/bluetooth tools/search_for_ble.py deleted file mode 100644 index e69de29..0000000 diff --git a/bluetooth tools/search_for_blc.py b/bluetooth_tools/search_for_blc.py similarity index 100% rename from bluetooth tools/search_for_blc.py rename to bluetooth_tools/search_for_blc.py diff --git a/bluetooth_tools/search_for_ble.py b/bluetooth_tools/search_for_ble.py new file mode 100644 index 0000000..8703b7a --- /dev/null +++ b/bluetooth_tools/search_for_ble.py @@ -0,0 +1,8 @@ +from bluepy.btle import Scanner +scanner = Scanner(0) +print('Start scan...') +devices = scanner.scan(10) +for device in devices: + print('address : %s' % device.addr.upper()) + print(device.getScanData()) + print('') diff --git a/log_to_json.py b/log_to_json.py new file mode 100644 index 0000000..7cef3ea --- /dev/null +++ b/log_to_json.py @@ -0,0 +1,13 @@ +import json + + +with open('history.txt', 'r') as file: + content = file.readlines() + +lines = [] +for line in content: + line = json.loads(line.strip()) + lines.append(line) + +with open('history.json', 'w') as file: + file.write(json.dumps(lines)) diff --git a/python/requierements.txt b/python/requierements.txt index d99d203..dd13488 100644 --- a/python/requierements.txt +++ b/python/requierements.txt @@ -1,2 +1,5 @@ bluepy -pyyaml \ No newline at end of file +pyyaml +bs4 +lxml +requests \ No newline at end of file diff --git a/python/src/data_class.py b/python/src/data_class.py index 9ddfcf8..79213e7 100644 --- a/python/src/data_class.py +++ b/python/src/data_class.py @@ -14,12 +14,10 @@ class Data: def parse_data(self, val): data_bytes = [int(val[i:i + 2], 16) for i in range(0, len(val), 2)] + if data_bytes[8] > 127: data_bytes[8] -= 256 - if data_bytes[8] > 127: - print(data_bytes) - data_bytes[8] -= 256 self.timestamp = datetime.now().astimezone().replace(microsecond=0).isoformat() - self.mac = ":".join(["{:02X}".format(data_bytes[i]) for i in range(2, 8)]) + self.mac = ":".join(["{:02X}".format(data_bytes[i]) for i in range(2, 8)]).upper() self.temperature = (data_bytes[8] * 256 + data_bytes[9]) / 10 self.humidity = data_bytes[10] self.battery_percent = data_bytes[11] diff --git a/python/src/devices.py b/python/src/devices.py index b5f480c..f0af17f 100644 --- a/python/src/devices.py +++ b/python/src/devices.py @@ -20,6 +20,5 @@ def get_devices(): return devices_list - - - +def get_device(dev): + return next((d for d in get_devices() if d.mac == dev.addr.upper()), None) diff --git a/python/src/discovery.py b/python/src/discovery.py index 2c965ff..88e1bbe 100644 --- a/python/src/discovery.py +++ b/python/src/discovery.py @@ -2,7 +2,7 @@ from bluepy.btle import DefaultDelegate from bluepy.btle import Scanner from data_class import Data -from devices import get_devices as get_device_from_config +from devices import get_device # This is the list, where the responses will be stored from the `handleDiscovery` devices = [] @@ -20,7 +20,7 @@ class ScanDelegate(DefaultDelegate): data_obj = Data(val) if self.is_atc_device(dev, data_obj): - device_from_config = self.get_device(dev) + device_from_config = get_device(dev) devices.append([dev, data_obj, device_from_config]) @staticmethod @@ -35,17 +35,13 @@ class ScanDelegate(DefaultDelegate): for device in devices: if str(device[0].addr) == str(dev.addr): return False - device_from_config = self.get_device(dev) + device_from_config = get_device(dev) try:print(f"Device: {dev.addr.upper()} ({dev.addrType}), RSSI: {dev.rssi} dB, Room: {device_from_config.room}") except:print(f"Device: {dev.addr.upper()} ({dev.addrType}), RSSI: {dev.rssi} dB, Room: ?") print(f'\tTemp: {data_obj.temperature}°C, Humid: {data_obj.humidity}%, Batt: {data_obj.battery_percent}%\n') return True - @staticmethod - def get_device(dev): - return next((d for d in get_device_from_config() if d.mac == dev.addr.upper()), None) - def cleanup(): global devices diff --git a/python/src/log_data.py b/python/src/log_data.py new file mode 100644 index 0000000..4640904 --- /dev/null +++ b/python/src/log_data.py @@ -0,0 +1 @@ +# TODO diff --git a/python/src/main.py b/python/src/main.py index 089dbe0..b2f60ab 100644 --- a/python/src/main.py +++ b/python/src/main.py @@ -1,7 +1,5 @@ from discovery import start_discovery -from data_class import Data from loop import start_loop -from time import sleep + devices = start_discovery() - diff --git a/python/src/mqtt.py b/python/src/mqtt.py new file mode 100644 index 0000000..4640904 --- /dev/null +++ b/python/src/mqtt.py @@ -0,0 +1 @@ +# TODO From 72762340ce190d5abcf694c67637e35a270609d6 Mon Sep 17 00:00:00 2001 From: DasMoorhuhn Date: Sun, 23 Jun 2024 00:42:26 +0200 Subject: [PATCH 04/10] added charts --- .gitignore | 3 +- Charts/data.json | 194 +++++++++++++++++++++++++++++++++++++++ Charts/index.html | 143 +++++++++++++++++++++++++++++ Charts/script.js | 107 +++++++++++++++++++++ Charts/styles.css | 20 ++++ python/src/data_class.py | 14 +-- python/src/devices.py | 7 ++ python/src/discovery.py | 7 +- python/src/log_data.py | 33 ++++++- python/src/main.py | 2 + 10 files changed, 518 insertions(+), 12 deletions(-) create mode 100644 Charts/data.json create mode 100644 Charts/index.html create mode 100644 Charts/script.js create mode 100644 Charts/styles.css diff --git a/.gitignore b/.gitignore index 8647625..8b10401 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ __pycache__ -devices.yml \ No newline at end of file +devices.yml +history.* \ No newline at end of file diff --git a/Charts/data.json b/Charts/data.json new file mode 100644 index 0000000..032d501 --- /dev/null +++ b/Charts/data.json @@ -0,0 +1,194 @@ +[ + { + "data": { + "timestamp": "2024-06-22T23:59:06+02:00", + "mac": "A4:C1:38:46:D7:75", + "temperature": 20.4, + "humidity": 69, + "battery_percent": 87, + "battery_volt": 2.985, + "count": 241 + }, + "device": { + "mac": "A4:C1:38:46:D7:75", + "name": "4", + "room": "dinner_room" + } + }, + { + "data": { + "timestamp": "2024-06-22T23:59:07+02:00", + "mac": "A4:C1:38:DD:9F:EB", + "temperature": 20.3, + "humidity": 70, + "battery_percent": 85, + "battery_volt": 2.975, + "count": 240 + }, + "device": { + "mac": "A4:C1:38:DD:9F:EB", + "name": "5", + "room": "kitchen" + } + }, + { + "data": { + "timestamp": "2024-06-22T23:59:07+02:00", + "mac": "A4:C1:38:9A:81:25", + "temperature": 25.1, + "humidity": 51, + "battery_percent": 82, + "battery_volt": 2.946, + "count": 143 + }, + "device": { + "mac": "A4:C1:38:9A:81:25", + "name": "6", + "room": "server" + } + }, + { + "data": { + "timestamp": "2024-06-22T23:59:09+02:00", + "mac": "A4:C1:38:83:05:E8", + "temperature": 22.2, + "humidity": 61, + "battery_percent": 80, + "battery_volt": 2.933, + "count": 32 + }, + "device": { + "mac": "A4:C1:38:83:05:E8", + "name": "my_room", + "room": "my_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:13:48+02:00", + "mac": "A4:C1:38:DD:9F:EB", + "temperature": 20.3, + "humidity": 70, + "battery_percent": 85, + "battery_volt": 2.974, + "count": 1 + }, + "device": { + "mac": "A4:C1:38:DD:9F:EB", + "name": "5", + "room": "kitchen" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:13:49+02:00", + "mac": "A4:C1:38:46:D7:75", + "temperature": 20.4, + "humidity": 69, + "battery_percent": 87, + "battery_volt": 2.986, + "count": 2 + }, + "device": { + "mac": "A4:C1:38:46:D7:75", + "name": "4", + "room": "dinner_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:13:50+02:00", + "mac": "A4:C1:38:83:05:E8", + "temperature": 22.2, + "humidity": 61, + "battery_percent": 80, + "battery_volt": 2.932, + "count": 90 + }, + "device": { + "mac": "A4:C1:38:83:05:E8", + "name": "my_room", + "room": "my_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:13:55+02:00", + "mac": "A4:C1:38:9A:81:25", + "temperature": 25.1, + "humidity": 51, + "battery_percent": 82, + "battery_volt": 2.945, + "count": 160 + }, + "device": { + "mac": "A4:C1:38:9A:81:25", + "name": "6", + "room": "server" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:14:11+02:00", + "mac": "A4:C1:38:83:05:E8", + "temperature": 22.3, + "humidity": 61, + "battery_percent": 81, + "battery_volt": 2.932, + "count": 92 + }, + "device": { + "mac": "A4:C1:38:83:05:E8", + "name": "my_room", + "room": "my_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:14:11+02:00", + "mac": "A4:C1:38:46:D7:75", + "temperature": 20.4, + "humidity": 69, + "battery_percent": 87, + "battery_volt": 2.986, + "count": 2 + }, + "device": { + "mac": "A4:C1:38:46:D7:75", + "name": "4", + "room": "dinner_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:14:12+02:00", + "mac": "A4:C1:38:9A:81:25", + "temperature": 25.1, + "humidity": 51, + "battery_percent": 82, + "battery_volt": 2.945, + "count": 160 + }, + "device": { + "mac": "A4:C1:38:9A:81:25", + "name": "6", + "room": "server" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:14:12+02:00", + "mac": "A4:C1:38:DD:9F:EB", + "temperature": 20.3, + "humidity": 70, + "battery_percent": 85, + "battery_volt": 2.974, + "count": 1 + }, + "device": { + "mac": "A4:C1:38:DD:9F:EB", + "name": "5", + "room": "kitchen" + } + } +] \ No newline at end of file diff --git a/Charts/index.html b/Charts/index.html new file mode 100644 index 0000000..1ff615a --- /dev/null +++ b/Charts/index.html @@ -0,0 +1,143 @@ + + + + + + Geräte-Diagramme + + + + +

Geräte-Diagramme

+
+ + + + diff --git a/Charts/script.js b/Charts/script.js new file mode 100644 index 0000000..2ba5ddc --- /dev/null +++ b/Charts/script.js @@ -0,0 +1,107 @@ +document.addEventListener('DOMContentLoaded', function () { + fetch('data.json') + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok ' + response.statusText); + } + return response.json(); + }) + .then(data => { + const devices = {}; + + data.forEach(entry => { + const mac = entry.device.mac; + if (!devices[mac]) { + devices[mac] = { + temperatureData: [], + humidityData: [], + labels: [], + name: entry.device.name, + room: entry.device.room + }; + } + devices[mac].labels.push(new Date(entry.data.timestamp)); + devices[mac].temperatureData.push(entry.data.temperature); + devices[mac].humidityData.push(entry.data.humidity); + }); + + const chartsContainer = document.getElementById('chartsContainer'); + + Object.keys(devices).forEach(mac => { + const device = devices[mac]; + + const chartContainer = document.createElement('div'); + chartContainer.className = 'chart-container'; + + const canvas = document.createElement('canvas'); + canvas.id = `chart-${mac}`; + + const title = document.createElement('h3'); + title.textContent = `Device ${device.name} in ${device.room}`; + + chartContainer.appendChild(title); + chartContainer.appendChild(canvas); + chartsContainer.appendChild(chartContainer); + + const ctx = canvas.getContext('2d'); + new Chart(ctx, { + type: 'line', + data: { + labels: device.labels, + datasets: [ + { + label: 'Temperatur (°C)', + data: device.temperatureData, + borderColor: 'rgba(255, 99, 132, 1)', + backgroundColor: 'rgba(255, 99, 132, 0.2)', + borderWidth: 1, + yAxisID: 'y1' + }, + { + label: 'Luftfeuchtigkeit (%)', + data: device.humidityData, + borderColor: 'rgba(54, 162, 235, 1)', + backgroundColor: 'rgba(54, 162, 235, 0.2)', + borderWidth: 1, + yAxisID: 'y2' + } + ] + }, + options: { + responsive: true, + scales: { + x: { + type: 'time', + time: { + unit: 'minute', + tooltipFormat: 'll HH:mm' + } + }, + y1: { + type: 'linear', + position: 'left', + beginAtZero: true, + title: { + display: true, + text: 'Temperatur (°C)' + } + }, + y2: { + type: 'linear', + position: 'right', + beginAtZero: true, + title: { + display: true, + text: 'Luftfeuchtigkeit (%)' + }, + grid: { + drawOnChartArea: false + } + } + } + } + }); + }); + }) + .catch(error => console.error('Error loading JSON data:', error)); +}); diff --git a/Charts/styles.css b/Charts/styles.css new file mode 100644 index 0000000..9cfcc97 --- /dev/null +++ b/Charts/styles.css @@ -0,0 +1,20 @@ +body { + font-family: Arial, sans-serif; + display: flex; + flex-direction: column; + align-items: center; + background-color: #f4f4f4; + margin: 0; + padding: 20px; +} + +.chart-container { + width: 80%; + margin: 20px 0; +} + +canvas { + background-color: #ffffff; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} diff --git a/python/src/data_class.py b/python/src/data_class.py index 79213e7..490c059 100644 --- a/python/src/data_class.py +++ b/python/src/data_class.py @@ -29,11 +29,11 @@ class Data: def to_json(self): return { - 'timestamp': self.timestamp, - 'mac': self.mac, - 'temperature': self.temperature, - 'humidity': self.humidity, - 'battery_percent': self.battery_percent, - 'battery_volt': self.battery_volt, - 'count': self.count, + "timestamp": self.timestamp, + "mac": self.mac, + "temperature": self.temperature, + "humidity": self.humidity, + "battery_percent": self.battery_percent, + "battery_volt": self.battery_volt, + "count": self.count, } diff --git a/python/src/devices.py b/python/src/devices.py index f0af17f..603b611 100644 --- a/python/src/devices.py +++ b/python/src/devices.py @@ -11,6 +11,13 @@ class Device: self.name = data['name'] self.room = data['room'] + def to_json(self): + return { + "mac": self.mac, + "name": self.name, + "room": self.room + } + def get_devices(): devices_list = [] diff --git a/python/src/discovery.py b/python/src/discovery.py index 88e1bbe..0b7451b 100644 --- a/python/src/discovery.py +++ b/python/src/discovery.py @@ -29,7 +29,8 @@ class ScanDelegate(DefaultDelegate): if len(val) != 30: return False return True - def is_atc_device(self, dev, data_obj): + @staticmethod + def is_atc_device(dev, data_obj): global devices if 'A4:C1:38' not in dev.addr.upper(): return False for device in devices: @@ -37,8 +38,8 @@ class ScanDelegate(DefaultDelegate): device_from_config = get_device(dev) - try:print(f"Device: {dev.addr.upper()} ({dev.addrType}), RSSI: {dev.rssi} dB, Room: {device_from_config.room}") - except:print(f"Device: {dev.addr.upper()} ({dev.addrType}), RSSI: {dev.rssi} dB, Room: ?") + try:print(f"Device: {dev.addr.upper()} ({dev.addrType}), RSSI: {dev.rssi}dB, Room: {device_from_config.room}") + except:print(f"Device: {dev.addr.upper()} ({dev.addrType}), RSSI: {dev.rssi}dB, Room: ?") print(f'\tTemp: {data_obj.temperature}°C, Humid: {data_obj.humidity}%, Batt: {data_obj.battery_percent}%\n') return True diff --git a/python/src/log_data.py b/python/src/log_data.py index 4640904..2724848 100644 --- a/python/src/log_data.py +++ b/python/src/log_data.py @@ -1 +1,32 @@ -# TODO +import os +import json +from data_class import Data +from devices import Device + +workdir, filename = os.path.split(os.path.abspath(__file__)) + + +def log_to_txt(devices): + with open(f'{workdir}{os.sep}history.json', 'r') as file: + data = json.load(file) + + with open(f'{workdir}{os.sep}history.json', 'w') as file: + for device in devices: + dev, data_obj, from_config = device + data_obj:Data + from_config:Device + data.append({ + "data": data_obj.to_json(), + "device": from_config.to_json() + }) + final_data = {"measurements": data} + file.write(json.dumps(data, indent=2)) + + +def log_to_mongodb(data): + pass + + +def log_to_mqtt(data): + pass + diff --git a/python/src/main.py b/python/src/main.py index b2f60ab..420c42b 100644 --- a/python/src/main.py +++ b/python/src/main.py @@ -1,5 +1,7 @@ from discovery import start_discovery from loop import start_loop +from log_data import log_to_txt devices = start_discovery() +log_to_txt(devices) From 9581a3ce2c78cca1024db387a19893057de934bf Mon Sep 17 00:00:00 2001 From: DasMoorhuhn Date: Thu, 27 Jun 2024 00:42:25 +0200 Subject: [PATCH 05/10] make docker image from 1GB to around 100MB --- .gitignore | 3 +- .idea/git_toolbox_blame.xml | 6 + vector.py => .media/vector.py | 0 Charts/data.json | 2594 +++++++++++++++++++++++++++++++-- Charts/index.html | 205 ++- Charts/script.js | 107 -- Charts/styles.css | 20 - Dockerfile | 38 +- README.md | 5 + docker-compose.yml | 2 +- log_to_json.py | 13 - python/docker_entrypoint.sh | 6 +- python/requierements.txt | 1 - python/src/log_data.py | 37 +- python/src/main.py | 4 +- run_docker.sh | 9 +- 16 files changed, 2693 insertions(+), 357 deletions(-) create mode 100644 .idea/git_toolbox_blame.xml rename vector.py => .media/vector.py (100%) delete mode 100644 Charts/script.js delete mode 100644 Charts/styles.css delete mode 100644 log_to_json.py diff --git a/.gitignore b/.gitignore index 8b10401..48b6f47 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ __pycache__ devices.yml -history.* \ No newline at end of file +history.* +data/ \ No newline at end of file diff --git a/.idea/git_toolbox_blame.xml b/.idea/git_toolbox_blame.xml new file mode 100644 index 0000000..7dc1249 --- /dev/null +++ b/.idea/git_toolbox_blame.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/vector.py b/.media/vector.py similarity index 100% rename from vector.py rename to .media/vector.py diff --git a/Charts/data.json b/Charts/data.json index 032d501..a71509c 100644 --- a/Charts/data.json +++ b/Charts/data.json @@ -1,45 +1,13 @@ [ { "data": { - "timestamp": "2024-06-22T23:59:06+02:00", - "mac": "A4:C1:38:46:D7:75", - "temperature": 20.4, - "humidity": 69, - "battery_percent": 87, - "battery_volt": 2.985, - "count": 241 - }, - "device": { - "mac": "A4:C1:38:46:D7:75", - "name": "4", - "room": "dinner_room" - } - }, - { - "data": { - "timestamp": "2024-06-22T23:59:07+02:00", - "mac": "A4:C1:38:DD:9F:EB", - "temperature": 20.3, - "humidity": 70, - "battery_percent": 85, - "battery_volt": 2.975, - "count": 240 - }, - "device": { - "mac": "A4:C1:38:DD:9F:EB", - "name": "5", - "room": "kitchen" - } - }, - { - "data": { - "timestamp": "2024-06-22T23:59:07+02:00", + "timestamp": "2024-06-23T00:51:02+02:00", "mac": "A4:C1:38:9A:81:25", "temperature": 25.1, "humidity": 51, "battery_percent": 82, "battery_volt": 2.946, - "count": 143 + "count": 202 }, "device": { "mac": "A4:C1:38:9A:81:25", @@ -49,45 +17,13 @@ }, { "data": { - "timestamp": "2024-06-22T23:59:09+02:00", - "mac": "A4:C1:38:83:05:E8", - "temperature": 22.2, - "humidity": 61, - "battery_percent": 80, - "battery_volt": 2.933, - "count": 32 - }, - "device": { - "mac": "A4:C1:38:83:05:E8", - "name": "my_room", - "room": "my_room" - } - }, - { - "data": { - "timestamp": "2024-06-23T00:13:48+02:00", - "mac": "A4:C1:38:DD:9F:EB", - "temperature": 20.3, - "humidity": 70, - "battery_percent": 85, - "battery_volt": 2.974, - "count": 1 - }, - "device": { - "mac": "A4:C1:38:DD:9F:EB", - "name": "5", - "room": "kitchen" - } - }, - { - "data": { - "timestamp": "2024-06-23T00:13:49+02:00", + "timestamp": "2024-06-23T00:51:02+02:00", "mac": "A4:C1:38:46:D7:75", - "temperature": 20.4, + "temperature": 20.3, "humidity": 69, "battery_percent": 87, - "battery_volt": 2.986, - "count": 2 + "battery_volt": 2.985, + "count": 44 }, "device": { "mac": "A4:C1:38:46:D7:75", @@ -97,13 +33,29 @@ }, { "data": { - "timestamp": "2024-06-23T00:13:50+02:00", + "timestamp": "2024-06-23T00:51:03+02:00", + "mac": "A4:C1:38:DD:9F:EB", + "temperature": 20.2, + "humidity": 70, + "battery_percent": 85, + "battery_volt": 2.975, + "count": 43 + }, + "device": { + "mac": "A4:C1:38:DD:9F:EB", + "name": "5", + "room": "kitchen" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:51:03+02:00", "mac": "A4:C1:38:83:05:E8", - "temperature": 22.2, - "humidity": 61, + "temperature": 22.3, + "humidity": 60, "battery_percent": 80, "battery_volt": 2.932, - "count": 90 + "count": 238 }, "device": { "mac": "A4:C1:38:83:05:E8", @@ -113,13 +65,93 @@ }, { "data": { - "timestamp": "2024-06-23T00:13:55+02:00", + "timestamp": "2024-06-23T00:51:04+02:00", + "mac": "A4:C1:38:D0:78:41", + "temperature": 20.0, + "humidity": 64, + "battery_percent": 86, + "battery_volt": 2.977, + "count": 63 + }, + "device": { + "mac": "A4:C1:38:D0:78:41", + "name": "2", + "room": "pantry" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:52:02+02:00", + "mac": "A4:C1:38:DD:9F:EB", + "temperature": 20.2, + "humidity": 70, + "battery_percent": 85, + "battery_volt": 2.975, + "count": 44 + }, + "device": { + "mac": "A4:C1:38:DD:9F:EB", + "name": "5", + "room": "kitchen" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:52:02+02:00", + "mac": "A4:C1:38:83:05:E8", + "temperature": 22.3, + "humidity": 60, + "battery_percent": 81, + "battery_volt": 2.933, + "count": 242 + }, + "device": { + "mac": "A4:C1:38:83:05:E8", + "name": "my_room", + "room": "my_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:52:02+02:00", + "mac": "A4:C1:38:D0:78:41", + "temperature": 20.0, + "humidity": 64, + "battery_percent": 86, + "battery_volt": 2.975, + "count": 65 + }, + "device": { + "mac": "A4:C1:38:D0:78:41", + "name": "2", + "room": "pantry" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:52:03+02:00", + "mac": "A4:C1:38:46:D7:75", + "temperature": 20.3, + "humidity": 69, + "battery_percent": 87, + "battery_volt": 2.985, + "count": 45 + }, + "device": { + "mac": "A4:C1:38:46:D7:75", + "name": "4", + "room": "dinner_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:52:05+02:00", "mac": "A4:C1:38:9A:81:25", "temperature": 25.1, "humidity": 51, "battery_percent": 82, - "battery_volt": 2.945, - "count": 160 + "battery_volt": 2.946, + "count": 203 }, "device": { "mac": "A4:C1:38:9A:81:25", @@ -129,12 +161,2092 @@ }, { "data": { - "timestamp": "2024-06-23T00:14:11+02:00", + "timestamp": "2024-06-23T00:53:01+02:00", + "mac": "A4:C1:38:9A:81:25", + "temperature": 25.1, + "humidity": 51, + "battery_percent": 82, + "battery_volt": 2.945, + "count": 204 + }, + "device": { + "mac": "A4:C1:38:9A:81:25", + "name": "6", + "room": "server" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:53:02+02:00", + "mac": "A4:C1:38:46:D7:75", + "temperature": 20.3, + "humidity": 69, + "battery_percent": 87, + "battery_volt": 2.985, + "count": 46 + }, + "device": { + "mac": "A4:C1:38:46:D7:75", + "name": "4", + "room": "dinner_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:53:03+02:00", + "mac": "A4:C1:38:D0:78:41", + "temperature": 20.0, + "humidity": 64, + "battery_percent": 86, + "battery_volt": 2.975, + "count": 66 + }, + "device": { + "mac": "A4:C1:38:D0:78:41", + "name": "2", + "room": "pantry" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:53:04+02:00", + "mac": "A4:C1:38:DD:9F:EB", + "temperature": 20.2, + "humidity": 70, + "battery_percent": 85, + "battery_volt": 2.975, + "count": 45 + }, + "device": { + "mac": "A4:C1:38:DD:9F:EB", + "name": "5", + "room": "kitchen" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:53:04+02:00", + "mac": "A4:C1:38:83:05:E8", + "temperature": 22.4, + "humidity": 60, + "battery_percent": 81, + "battery_volt": 2.933, + "count": 246 + }, + "device": { + "mac": "A4:C1:38:83:05:E8", + "name": "my_room", + "room": "my_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:54:02+02:00", + "mac": "A4:C1:38:9A:81:25", + "temperature": 25.1, + "humidity": 51, + "battery_percent": 82, + "battery_volt": 2.945, + "count": 205 + }, + "device": { + "mac": "A4:C1:38:9A:81:25", + "name": "6", + "room": "server" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:54:02+02:00", + "mac": "A4:C1:38:46:D7:75", + "temperature": 20.3, + "humidity": 69, + "battery_percent": 87, + "battery_volt": 2.985, + "count": 47 + }, + "device": { + "mac": "A4:C1:38:46:D7:75", + "name": "4", + "room": "dinner_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:54:02+02:00", + "mac": "A4:C1:38:DD:9F:EB", + "temperature": 20.2, + "humidity": 70, + "battery_percent": 85, + "battery_volt": 2.973, + "count": 47 + }, + "device": { + "mac": "A4:C1:38:DD:9F:EB", + "name": "5", + "room": "kitchen" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:54:03+02:00", + "mac": "A4:C1:38:83:05:E8", + "temperature": 22.4, + "humidity": 60, + "battery_percent": 81, + "battery_volt": 2.933, + "count": 250 + }, + "device": { + "mac": "A4:C1:38:83:05:E8", + "name": "my_room", + "room": "my_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:54:03+02:00", + "mac": "A4:C1:38:D0:78:41", + "temperature": 20.0, + "humidity": 64, + "battery_percent": 86, + "battery_volt": 2.975, + "count": 67 + }, + "device": { + "mac": "A4:C1:38:D0:78:41", + "name": "2", + "room": "pantry" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:55:02+02:00", + "mac": "A4:C1:38:9A:81:25", + "temperature": 25.1, + "humidity": 51, + "battery_percent": 82, + "battery_volt": 2.946, + "count": 207 + }, + "device": { + "mac": "A4:C1:38:9A:81:25", + "name": "6", + "room": "server" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:55:03+02:00", + "mac": "A4:C1:38:DD:9F:EB", + "temperature": 20.2, + "humidity": 70, + "battery_percent": 85, + "battery_volt": 2.973, + "count": 48 + }, + "device": { + "mac": "A4:C1:38:DD:9F:EB", + "name": "5", + "room": "kitchen" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:55:03+02:00", "mac": "A4:C1:38:83:05:E8", "temperature": 22.3, - "humidity": 61, - "battery_percent": 81, + "humidity": 60, + "battery_percent": 80, "battery_volt": 2.932, + "count": 254 + }, + "device": { + "mac": "A4:C1:38:83:05:E8", + "name": "my_room", + "room": "my_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:55:04+02:00", + "mac": "A4:C1:38:46:D7:75", + "temperature": 20.3, + "humidity": 69, + "battery_percent": 87, + "battery_volt": 2.983, + "count": 49 + }, + "device": { + "mac": "A4:C1:38:46:D7:75", + "name": "4", + "room": "dinner_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:55:05+02:00", + "mac": "A4:C1:38:D0:78:41", + "temperature": 20.0, + "humidity": 64, + "battery_percent": 86, + "battery_volt": 2.975, + "count": 68 + }, + "device": { + "mac": "A4:C1:38:D0:78:41", + "name": "2", + "room": "pantry" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:56:02+02:00", + "mac": "A4:C1:38:DD:9F:EB", + "temperature": 20.3, + "humidity": 70, + "battery_percent": 85, + "battery_volt": 2.973, + "count": 49 + }, + "device": { + "mac": "A4:C1:38:DD:9F:EB", + "name": "5", + "room": "kitchen" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:56:02+02:00", + "mac": "A4:C1:38:83:05:E8", + "temperature": 22.3, + "humidity": 60, + "battery_percent": 80, + "battery_volt": 2.932, + "count": 2 + }, + "device": { + "mac": "A4:C1:38:83:05:E8", + "name": "my_room", + "room": "my_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:56:02+02:00", + "mac": "A4:C1:38:D0:78:41", + "temperature": 20.0, + "humidity": 64, + "battery_percent": 86, + "battery_volt": 2.975, + "count": 69 + }, + "device": { + "mac": "A4:C1:38:D0:78:41", + "name": "2", + "room": "pantry" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:56:03+02:00", + "mac": "A4:C1:38:46:D7:75", + "temperature": 20.3, + "humidity": 69, + "battery_percent": 87, + "battery_volt": 2.983, + "count": 50 + }, + "device": { + "mac": "A4:C1:38:46:D7:75", + "name": "4", + "room": "dinner_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:56:05+02:00", + "mac": "A4:C1:38:9A:81:25", + "temperature": 25.1, + "humidity": 51, + "battery_percent": 82, + "battery_volt": 2.946, + "count": 208 + }, + "device": { + "mac": "A4:C1:38:9A:81:25", + "name": "6", + "room": "server" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:57:02+02:00", + "mac": "A4:C1:38:DD:9F:EB", + "temperature": 20.3, + "humidity": 70, + "battery_percent": 85, + "battery_volt": 2.974, + "count": 50 + }, + "device": { + "mac": "A4:C1:38:DD:9F:EB", + "name": "5", + "room": "kitchen" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:57:02+02:00", + "mac": "A4:C1:38:83:05:E8", + "temperature": 22.1, + "humidity": 55, + "battery_percent": 80, + "battery_volt": 2.932, + "count": 6 + }, + "device": { + "mac": "A4:C1:38:83:05:E8", + "name": "my_room", + "room": "my_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:57:02+02:00", + "mac": "A4:C1:38:D0:78:41", + "temperature": 20.0, + "humidity": 64, + "battery_percent": 86, + "battery_volt": 2.975, + "count": 70 + }, + "device": { + "mac": "A4:C1:38:D0:78:41", + "name": "2", + "room": "pantry" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:57:03+02:00", + "mac": "A4:C1:38:46:D7:75", + "temperature": 20.3, + "humidity": 69, + "battery_percent": 87, + "battery_volt": 2.983, + "count": 51 + }, + "device": { + "mac": "A4:C1:38:46:D7:75", + "name": "4", + "room": "dinner_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:57:07+02:00", + "mac": "A4:C1:38:9A:81:25", + "temperature": 25.1, + "humidity": 51, + "battery_percent": 82, + "battery_volt": 2.946, + "count": 209 + }, + "device": { + "mac": "A4:C1:38:9A:81:25", + "name": "6", + "room": "server" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:58:02+02:00", + "mac": "A4:C1:38:9A:81:25", + "temperature": 25.1, + "humidity": 51, + "battery_percent": 82, + "battery_volt": 2.946, + "count": 210 + }, + "device": { + "mac": "A4:C1:38:9A:81:25", + "name": "6", + "room": "server" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:58:02+02:00", + "mac": "A4:C1:38:46:D7:75", + "temperature": 20.3, + "humidity": 69, + "battery_percent": 87, + "battery_volt": 2.983, + "count": 52 + }, + "device": { + "mac": "A4:C1:38:46:D7:75", + "name": "4", + "room": "dinner_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:58:02+02:00", + "mac": "A4:C1:38:DD:9F:EB", + "temperature": 20.3, + "humidity": 70, + "battery_percent": 85, + "battery_volt": 2.974, + "count": 51 + }, + "device": { + "mac": "A4:C1:38:DD:9F:EB", + "name": "5", + "room": "kitchen" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:58:03+02:00", + "mac": "A4:C1:38:83:05:E8", + "temperature": 20.3, + "humidity": 55, + "battery_percent": 80, + "battery_volt": 2.926, + "count": 11 + }, + "device": { + "mac": "A4:C1:38:83:05:E8", + "name": "my_room", + "room": "my_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:58:03+02:00", + "mac": "A4:C1:38:D0:78:41", + "temperature": 20.0, + "humidity": 64, + "battery_percent": 86, + "battery_volt": 2.976, + "count": 71 + }, + "device": { + "mac": "A4:C1:38:D0:78:41", + "name": "2", + "room": "pantry" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:59:02+02:00", + "mac": "A4:C1:38:9A:81:25", + "temperature": 25.1, + "humidity": 51, + "battery_percent": 82, + "battery_volt": 2.946, + "count": 211 + }, + "device": { + "mac": "A4:C1:38:9A:81:25", + "name": "6", + "room": "server" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:59:03+02:00", + "mac": "A4:C1:38:DD:9F:EB", + "temperature": 20.2, + "humidity": 70, + "battery_percent": 85, + "battery_volt": 2.974, + "count": 52 + }, + "device": { + "mac": "A4:C1:38:DD:9F:EB", + "name": "5", + "room": "kitchen" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:59:03+02:00", + "mac": "A4:C1:38:83:05:E8", + "temperature": 20.3, + "humidity": 55, + "battery_percent": 80, + "battery_volt": 2.926, + "count": 15 + }, + "device": { + "mac": "A4:C1:38:83:05:E8", + "name": "my_room", + "room": "my_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:59:03+02:00", + "mac": "A4:C1:38:D0:78:41", + "temperature": 20.0, + "humidity": 64, + "battery_percent": 86, + "battery_volt": 2.976, + "count": 73 + }, + "device": { + "mac": "A4:C1:38:D0:78:41", + "name": "2", + "room": "pantry" + } + }, + { + "data": { + "timestamp": "2024-06-23T00:59:04+02:00", + "mac": "A4:C1:38:46:D7:75", + "temperature": 20.3, + "humidity": 69, + "battery_percent": 87, + "battery_volt": 2.986, + "count": 53 + }, + "device": { + "mac": "A4:C1:38:46:D7:75", + "name": "4", + "room": "dinner_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:00:02+02:00", + "mac": "A4:C1:38:D0:78:41", + "temperature": 20.0, + "humidity": 64, + "battery_percent": 86, + "battery_volt": 2.976, + "count": 74 + }, + "device": { + "mac": "A4:C1:38:D0:78:41", + "name": "2", + "room": "pantry" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:00:03+02:00", + "mac": "A4:C1:38:9A:81:25", + "temperature": 25.1, + "humidity": 51, + "battery_percent": 82, + "battery_volt": 2.946, + "count": 212 + }, + "device": { + "mac": "A4:C1:38:9A:81:25", + "name": "6", + "room": "server" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:00:03+02:00", + "mac": "A4:C1:38:46:D7:75", + "temperature": 20.3, + "humidity": 69, + "battery_percent": 87, + "battery_volt": 2.986, + "count": 54 + }, + "device": { + "mac": "A4:C1:38:46:D7:75", + "name": "4", + "room": "dinner_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:00:05+02:00", + "mac": "A4:C1:38:DD:9F:EB", + "temperature": 20.2, + "humidity": 70, + "battery_percent": 85, + "battery_volt": 2.974, + "count": 53 + }, + "device": { + "mac": "A4:C1:38:DD:9F:EB", + "name": "5", + "room": "kitchen" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:00:05+02:00", + "mac": "A4:C1:38:83:05:E8", + "temperature": 18.6, + "humidity": 58, + "battery_percent": 80, + "battery_volt": 2.926, + "count": 20 + }, + "device": { + "mac": "A4:C1:38:83:05:E8", + "name": "my_room", + "room": "my_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:01:01+02:00", + "mac": "A4:C1:38:9A:81:25", + "temperature": 25.1, + "humidity": 51, + "battery_percent": 82, + "battery_volt": 2.946, + "count": 213 + }, + "device": { + "mac": "A4:C1:38:9A:81:25", + "name": "6", + "room": "server" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:01:01+02:00", + "mac": "A4:C1:38:46:D7:75", + "temperature": 20.3, + "humidity": 69, + "battery_percent": 87, + "battery_volt": 2.986, + "count": 55 + }, + "device": { + "mac": "A4:C1:38:46:D7:75", + "name": "4", + "room": "dinner_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:01:02+02:00", + "mac": "A4:C1:38:DD:9F:EB", + "temperature": 20.2, + "humidity": 70, + "battery_percent": 85, + "battery_volt": 2.973, + "count": 55 + }, + "device": { + "mac": "A4:C1:38:DD:9F:EB", + "name": "5", + "room": "kitchen" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:01:02+02:00", + "mac": "A4:C1:38:83:05:E8", + "temperature": 17.3, + "humidity": 62, + "battery_percent": 77, + "battery_volt": 2.902, + "count": 25 + }, + "device": { + "mac": "A4:C1:38:83:05:E8", + "name": "my_room", + "room": "my_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:01:02+02:00", + "mac": "A4:C1:38:D0:78:41", + "temperature": 20.0, + "humidity": 64, + "battery_percent": 86, + "battery_volt": 2.976, + "count": 75 + }, + "device": { + "mac": "A4:C1:38:D0:78:41", + "name": "2", + "room": "pantry" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:02:02+02:00", + "mac": "A4:C1:38:9A:81:25", + "temperature": 25.1, + "humidity": 51, + "battery_percent": 82, + "battery_volt": 2.946, + "count": 214 + }, + "device": { + "mac": "A4:C1:38:9A:81:25", + "name": "6", + "room": "server" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:02:02+02:00", + "mac": "A4:C1:38:46:D7:75", + "temperature": 20.3, + "humidity": 69, + "battery_percent": 87, + "battery_volt": 2.985, + "count": 56 + }, + "device": { + "mac": "A4:C1:38:46:D7:75", + "name": "4", + "room": "dinner_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:02:03+02:00", + "mac": "A4:C1:38:DD:9F:EB", + "temperature": 20.2, + "humidity": 70, + "battery_percent": 85, + "battery_volt": 2.973, + "count": 56 + }, + "device": { + "mac": "A4:C1:38:DD:9F:EB", + "name": "5", + "room": "kitchen" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:02:03+02:00", + "mac": "A4:C1:38:83:05:E8", + "temperature": 16.3, + "humidity": 64, + "battery_percent": 77, + "battery_volt": 2.902, + "count": 30 + }, + "device": { + "mac": "A4:C1:38:83:05:E8", + "name": "my_room", + "room": "my_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:02:03+02:00", + "mac": "A4:C1:38:D0:78:41", + "temperature": 20.0, + "humidity": 64, + "battery_percent": 86, + "battery_volt": 2.976, + "count": 76 + }, + "device": { + "mac": "A4:C1:38:D0:78:41", + "name": "2", + "room": "pantry" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:03:01+02:00", + "mac": "A4:C1:38:83:05:E8", + "temperature": 15.5, + "humidity": 67, + "battery_percent": 77, + "battery_volt": 2.902, + "count": 35 + }, + "device": { + "mac": "A4:C1:38:83:05:E8", + "name": "my_room", + "room": "my_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:03:01+02:00", + "mac": "A4:C1:38:D0:78:41", + "temperature": 20.0, + "humidity": 64, + "battery_percent": 86, + "battery_volt": 2.976, + "count": 77 + }, + "device": { + "mac": "A4:C1:38:D0:78:41", + "name": "2", + "room": "pantry" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:03:02+02:00", + "mac": "A4:C1:38:BB:98:5C", + "temperature": 20.2, + "humidity": 71, + "battery_percent": 87, + "battery_volt": 2.988, + "count": 65 + }, + "device": { + "mac": "A4:C1:38:BB:98:5C", + "name": "3", + "room": "living_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:03:02+02:00", + "mac": "A4:C1:38:9A:81:25", + "temperature": 25.1, + "humidity": 51, + "battery_percent": 82, + "battery_volt": 2.947, + "count": 216 + }, + "device": { + "mac": "A4:C1:38:9A:81:25", + "name": "6", + "room": "server" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:03:03+02:00", + "mac": "A4:C1:38:46:D7:75", + "temperature": 20.3, + "humidity": 69, + "battery_percent": 87, + "battery_volt": 2.985, + "count": 58 + }, + "device": { + "mac": "A4:C1:38:46:D7:75", + "name": "4", + "room": "dinner_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:03:03+02:00", + "mac": "A4:C1:38:DD:9F:EB", + "temperature": 20.2, + "humidity": 70, + "battery_percent": 85, + "battery_volt": 2.973, + "count": 57 + }, + "device": { + "mac": "A4:C1:38:DD:9F:EB", + "name": "5", + "room": "kitchen" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:04:01+02:00", + "mac": "A4:C1:38:DD:9F:EB", + "temperature": 20.2, + "humidity": 70, + "battery_percent": 85, + "battery_volt": 2.973, + "count": 58 + }, + "device": { + "mac": "A4:C1:38:DD:9F:EB", + "name": "5", + "room": "kitchen" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:04:02+02:00", + "mac": "A4:C1:38:83:05:E8", + "temperature": 15.5, + "humidity": 67, + "battery_percent": 75, + "battery_volt": 2.885, + "count": 39 + }, + "device": { + "mac": "A4:C1:38:83:05:E8", + "name": "my_room", + "room": "my_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:04:03+02:00", + "mac": "A4:C1:38:9A:81:25", + "temperature": 25.1, + "humidity": 51, + "battery_percent": 82, + "battery_volt": 2.947, + "count": 217 + }, + "device": { + "mac": "A4:C1:38:9A:81:25", + "name": "6", + "room": "server" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:04:03+02:00", + "mac": "A4:C1:38:46:D7:75", + "temperature": 20.3, + "humidity": 69, + "battery_percent": 87, + "battery_volt": 2.985, + "count": 59 + }, + "device": { + "mac": "A4:C1:38:46:D7:75", + "name": "4", + "room": "dinner_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:04:04+02:00", + "mac": "A4:C1:38:D0:78:41", + "temperature": 20.0, + "humidity": 64, + "battery_percent": 86, + "battery_volt": 2.977, + "count": 78 + }, + "device": { + "mac": "A4:C1:38:D0:78:41", + "name": "2", + "room": "pantry" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:05:02+02:00", + "mac": "A4:C1:38:83:05:E8", + "temperature": 14.9, + "humidity": 69, + "battery_percent": 75, + "battery_volt": 2.885, + "count": 44 + }, + "device": { + "mac": "A4:C1:38:83:05:E8", + "name": "my_room", + "room": "my_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:05:02+02:00", + "mac": "A4:C1:38:DD:9F:EB", + "temperature": 20.3, + "humidity": 70, + "battery_percent": 85, + "battery_volt": 2.973, + "count": 59 + }, + "device": { + "mac": "A4:C1:38:DD:9F:EB", + "name": "5", + "room": "kitchen" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:05:03+02:00", + "mac": "A4:C1:38:9A:81:25", + "temperature": 25.1, + "humidity": 51, + "battery_percent": 82, + "battery_volt": 2.947, + "count": 218 + }, + "device": { + "mac": "A4:C1:38:9A:81:25", + "name": "6", + "room": "server" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:05:04+02:00", + "mac": "A4:C1:38:46:D7:75", + "temperature": 20.3, + "humidity": 69, + "battery_percent": 87, + "battery_volt": 2.985, + "count": 60 + }, + "device": { + "mac": "A4:C1:38:46:D7:75", + "name": "4", + "room": "dinner_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:05:04+02:00", + "mac": "A4:C1:38:D0:78:41", + "temperature": 20.0, + "humidity": 64, + "battery_percent": 86, + "battery_volt": 2.977, + "count": 79 + }, + "device": { + "mac": "A4:C1:38:D0:78:41", + "name": "2", + "room": "pantry" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:06:01+02:00", + "mac": "A4:C1:38:D0:78:41", + "temperature": 20.0, + "humidity": 64, + "battery_percent": 86, + "battery_volt": 2.977, + "count": 80 + }, + "device": { + "mac": "A4:C1:38:D0:78:41", + "name": "2", + "room": "pantry" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:06:02+02:00", + "mac": "A4:C1:38:9A:81:25", + "temperature": 25.1, + "humidity": 51, + "battery_percent": 82, + "battery_volt": 2.947, + "count": 219 + }, + "device": { + "mac": "A4:C1:38:9A:81:25", + "name": "6", + "room": "server" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:06:02+02:00", + "mac": "A4:C1:38:46:D7:75", + "temperature": 20.3, + "humidity": 69, + "battery_percent": 87, + "battery_volt": 2.985, + "count": 61 + }, + "device": { + "mac": "A4:C1:38:46:D7:75", + "name": "4", + "room": "dinner_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:06:02+02:00", + "mac": "A4:C1:38:83:05:E8", + "temperature": 14.3, + "humidity": 70, + "battery_percent": 75, + "battery_volt": 2.885, + "count": 49 + }, + "device": { + "mac": "A4:C1:38:83:05:E8", + "name": "my_room", + "room": "my_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:06:03+02:00", + "mac": "A4:C1:38:DD:9F:EB", + "temperature": 20.2, + "humidity": 70, + "battery_percent": 85, + "battery_volt": 2.973, + "count": 60 + }, + "device": { + "mac": "A4:C1:38:DD:9F:EB", + "name": "5", + "room": "kitchen" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:07:01+02:00", + "mac": "A4:C1:38:D0:78:41", + "temperature": 20.0, + "humidity": 64, + "battery_percent": 86, + "battery_volt": 2.976, + "count": 82 + }, + "device": { + "mac": "A4:C1:38:D0:78:41", + "name": "2", + "room": "pantry" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:07:03+02:00", + "mac": "A4:C1:38:46:D7:75", + "temperature": 20.3, + "humidity": 69, + "battery_percent": 87, + "battery_volt": 2.985, + "count": 62 + }, + "device": { + "mac": "A4:C1:38:46:D7:75", + "name": "4", + "room": "dinner_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:07:03+02:00", + "mac": "A4:C1:38:83:05:E8", + "temperature": 13.9, + "humidity": 72, + "battery_percent": 73, + "battery_volt": 2.872, + "count": 53 + }, + "device": { + "mac": "A4:C1:38:83:05:E8", + "name": "my_room", + "room": "my_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:07:04+02:00", + "mac": "A4:C1:38:9A:81:25", + "temperature": 25.1, + "humidity": 51, + "battery_percent": 82, + "battery_volt": 2.947, + "count": 220 + }, + "device": { + "mac": "A4:C1:38:9A:81:25", + "name": "6", + "room": "server" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:07:07+02:00", + "mac": "A4:C1:38:DD:9F:EB", + "temperature": 20.2, + "humidity": 70, + "battery_percent": 85, + "battery_volt": 2.971, + "count": 61 + }, + "device": { + "mac": "A4:C1:38:DD:9F:EB", + "name": "5", + "room": "kitchen" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:08:02+02:00", + "mac": "A4:C1:38:DD:9F:EB", + "temperature": 20.2, + "humidity": 70, + "battery_percent": 85, + "battery_volt": 2.971, + "count": 62 + }, + "device": { + "mac": "A4:C1:38:DD:9F:EB", + "name": "5", + "room": "kitchen" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:08:02+02:00", + "mac": "A4:C1:38:D0:78:41", + "temperature": 20.0, + "humidity": 64, + "battery_percent": 86, + "battery_volt": 2.976, + "count": 83 + }, + "device": { + "mac": "A4:C1:38:D0:78:41", + "name": "2", + "room": "pantry" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:08:03+02:00", + "mac": "A4:C1:38:9A:81:25", + "temperature": 25.1, + "humidity": 51, + "battery_percent": 82, + "battery_volt": 2.947, + "count": 221 + }, + "device": { + "mac": "A4:C1:38:9A:81:25", + "name": "6", + "room": "server" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:08:03+02:00", + "mac": "A4:C1:38:83:05:E8", + "temperature": 13.5, + "humidity": 73, + "battery_percent": 73, + "battery_volt": 2.872, + "count": 57 + }, + "device": { + "mac": "A4:C1:38:83:05:E8", + "name": "my_room", + "room": "my_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:08:05+02:00", + "mac": "A4:C1:38:46:D7:75", + "temperature": 20.3, + "humidity": 69, + "battery_percent": 87, + "battery_volt": 2.985, + "count": 63 + }, + "device": { + "mac": "A4:C1:38:46:D7:75", + "name": "4", + "room": "dinner_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:09:01+02:00", + "mac": "A4:C1:38:9A:81:25", + "temperature": 25.1, + "humidity": 51, + "battery_percent": 82, + "battery_volt": 2.946, + "count": 222 + }, + "device": { + "mac": "A4:C1:38:9A:81:25", + "name": "6", + "room": "server" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:09:02+02:00", + "mac": "A4:C1:38:46:D7:75", + "temperature": 20.3, + "humidity": 69, + "battery_percent": 87, + "battery_volt": 2.985, + "count": 64 + }, + "device": { + "mac": "A4:C1:38:46:D7:75", + "name": "4", + "room": "dinner_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:09:02+02:00", + "mac": "A4:C1:38:83:05:E8", + "temperature": 13.5, + "humidity": 73, + "battery_percent": 73, + "battery_volt": 2.872, + "count": 61 + }, + "device": { + "mac": "A4:C1:38:83:05:E8", + "name": "my_room", + "room": "my_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:09:03+02:00", + "mac": "A4:C1:38:D0:78:41", + "temperature": 20.0, + "humidity": 64, + "battery_percent": 86, + "battery_volt": 2.976, + "count": 84 + }, + "device": { + "mac": "A4:C1:38:D0:78:41", + "name": "2", + "room": "pantry" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:09:08+02:00", + "mac": "A4:C1:38:DD:9F:EB", + "temperature": 20.2, + "humidity": 70, + "battery_percent": 85, + "battery_volt": 2.974, + "count": 64 + }, + "device": { + "mac": "A4:C1:38:DD:9F:EB", + "name": "5", + "room": "kitchen" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:10:02+02:00", + "mac": "A4:C1:38:9A:81:25", + "temperature": 25.1, + "humidity": 51, + "battery_percent": 82, + "battery_volt": 2.946, + "count": 224 + }, + "device": { + "mac": "A4:C1:38:9A:81:25", + "name": "6", + "room": "server" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:10:02+02:00", + "mac": "A4:C1:38:46:D7:75", + "temperature": 20.3, + "humidity": 69, + "battery_percent": 87, + "battery_volt": 2.985, + "count": 65 + }, + "device": { + "mac": "A4:C1:38:46:D7:75", + "name": "4", + "room": "dinner_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:10:03+02:00", + "mac": "A4:C1:38:DD:9F:EB", + "temperature": 20.2, + "humidity": 70, + "battery_percent": 85, + "battery_volt": 2.974, + "count": 65 + }, + "device": { + "mac": "A4:C1:38:DD:9F:EB", + "name": "5", + "room": "kitchen" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:10:04+02:00", + "mac": "A4:C1:38:83:05:E8", + "temperature": 13.2, + "humidity": 74, + "battery_percent": 73, + "battery_volt": 2.867, + "count": 65 + }, + "device": { + "mac": "A4:C1:38:83:05:E8", + "name": "my_room", + "room": "my_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:10:07+02:00", + "mac": "A4:C1:38:D0:78:41", + "temperature": 20.0, + "humidity": 64, + "battery_percent": 86, + "battery_volt": 2.977, + "count": 85 + }, + "device": { + "mac": "A4:C1:38:D0:78:41", + "name": "2", + "room": "pantry" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:10:13+02:00", + "mac": "A4:C1:38:BB:98:5C", + "temperature": 20.2, + "humidity": 71, + "battery_percent": 87, + "battery_volt": 2.987, + "count": 74 + }, + "device": { + "mac": "A4:C1:38:BB:98:5C", + "name": "3", + "room": "living_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:11:02+02:00", + "mac": "A4:C1:38:9A:81:25", + "temperature": 25.1, + "humidity": 51, + "battery_percent": 82, + "battery_volt": 2.947, + "count": 225 + }, + "device": { + "mac": "A4:C1:38:9A:81:25", + "name": "6", + "room": "server" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:11:03+02:00", + "mac": "A4:C1:38:46:D7:75", + "temperature": 20.3, + "humidity": 69, + "battery_percent": 87, + "battery_volt": 2.985, + "count": 67 + }, + "device": { + "mac": "A4:C1:38:46:D7:75", + "name": "4", + "room": "dinner_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:11:03+02:00", + "mac": "A4:C1:38:83:05:E8", + "temperature": 12.9, + "humidity": 76, + "battery_percent": 73, + "battery_volt": 2.867, + "count": 69 + }, + "device": { + "mac": "A4:C1:38:83:05:E8", + "name": "my_room", + "room": "my_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:11:03+02:00", + "mac": "A4:C1:38:DD:9F:EB", + "temperature": 20.2, + "humidity": 70, + "battery_percent": 85, + "battery_volt": 2.974, + "count": 66 + }, + "device": { + "mac": "A4:C1:38:DD:9F:EB", + "name": "5", + "room": "kitchen" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:11:03+02:00", + "mac": "A4:C1:38:D0:78:41", + "temperature": 20.0, + "humidity": 64, + "battery_percent": 86, + "battery_volt": 2.977, + "count": 86 + }, + "device": { + "mac": "A4:C1:38:D0:78:41", + "name": "2", + "room": "pantry" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:12:02+02:00", + "mac": "A4:C1:38:DD:9F:EB", + "temperature": 20.2, + "humidity": 70, + "battery_percent": 85, + "battery_volt": 2.974, + "count": 67 + }, + "device": { + "mac": "A4:C1:38:DD:9F:EB", + "name": "5", + "room": "kitchen" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:12:02+02:00", + "mac": "A4:C1:38:D0:78:41", + "temperature": 20.0, + "humidity": 64, + "battery_percent": 86, + "battery_volt": 2.977, + "count": 87 + }, + "device": { + "mac": "A4:C1:38:D0:78:41", + "name": "2", + "room": "pantry" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:12:03+02:00", + "mac": "A4:C1:38:9A:81:25", + "temperature": 25.1, + "humidity": 51, + "battery_percent": 82, + "battery_volt": 2.947, + "count": 226 + }, + "device": { + "mac": "A4:C1:38:9A:81:25", + "name": "6", + "room": "server" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:12:03+02:00", + "mac": "A4:C1:38:83:05:E8", + "temperature": 12.7, + "humidity": 77, + "battery_percent": 73, + "battery_volt": 2.867, + "count": 73 + }, + "device": { + "mac": "A4:C1:38:83:05:E8", + "name": "my_room", + "room": "my_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:12:05+02:00", + "mac": "A4:C1:38:46:D7:75", + "temperature": 20.3, + "humidity": 69, + "battery_percent": 87, + "battery_volt": 2.985, + "count": 68 + }, + "device": { + "mac": "A4:C1:38:46:D7:75", + "name": "4", + "room": "dinner_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:12:08+02:00", + "mac": "A4:C1:38:BB:98:5C", + "temperature": 20.1, + "humidity": 71, + "battery_percent": 87, + "battery_volt": 2.99, + "count": 76 + }, + "device": { + "mac": "A4:C1:38:BB:98:5C", + "name": "3", + "room": "living_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:13:01+02:00", + "mac": "A4:C1:38:9A:81:25", + "temperature": 25.1, + "humidity": 51, + "battery_percent": 82, + "battery_volt": 2.947, + "count": 227 + }, + "device": { + "mac": "A4:C1:38:9A:81:25", + "name": "6", + "room": "server" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:13:02+02:00", + "mac": "A4:C1:38:46:D7:75", + "temperature": 20.3, + "humidity": 69, + "battery_percent": 87, + "battery_volt": 2.985, + "count": 69 + }, + "device": { + "mac": "A4:C1:38:46:D7:75", + "name": "4", + "room": "dinner_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:13:02+02:00", + "mac": "A4:C1:38:D0:78:41", + "temperature": 20.0, + "humidity": 64, + "battery_percent": 86, + "battery_volt": 2.977, + "count": 88 + }, + "device": { + "mac": "A4:C1:38:D0:78:41", + "name": "2", + "room": "pantry" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:13:04+02:00", + "mac": "A4:C1:38:83:05:E8", + "temperature": 12.6, + "humidity": 78, + "battery_percent": 73, + "battery_volt": 2.867, + "count": 77 + }, + "device": { + "mac": "A4:C1:38:83:05:E8", + "name": "my_room", + "room": "my_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:13:04+02:00", + "mac": "A4:C1:38:DD:9F:EB", + "temperature": 20.2, + "humidity": 70, + "battery_percent": 85, + "battery_volt": 2.975, + "count": 68 + }, + "device": { + "mac": "A4:C1:38:DD:9F:EB", + "name": "5", + "room": "kitchen" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:13:05+02:00", + "mac": "A4:C1:38:BB:98:5C", + "temperature": 20.2, + "humidity": 71, + "battery_percent": 87, + "battery_volt": 2.99, + "count": 77 + }, + "device": { + "mac": "A4:C1:38:BB:98:5C", + "name": "3", + "room": "living_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:14:02+02:00", + "mac": "A4:C1:38:9A:81:25", + "temperature": 25.1, + "humidity": 51, + "battery_percent": 82, + "battery_volt": 2.947, + "count": 228 + }, + "device": { + "mac": "A4:C1:38:9A:81:25", + "name": "6", + "room": "server" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:14:02+02:00", + "mac": "A4:C1:38:46:D7:75", + "temperature": 20.3, + "humidity": 69, + "battery_percent": 87, + "battery_volt": 2.984, + "count": 70 + }, + "device": { + "mac": "A4:C1:38:46:D7:75", + "name": "4", + "room": "dinner_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:14:02+02:00", + "mac": "A4:C1:38:83:05:E8", + "temperature": 12.6, + "humidity": 78, + "battery_percent": 72, + "battery_volt": 2.862, + "count": 80 + }, + "device": { + "mac": "A4:C1:38:83:05:E8", + "name": "my_room", + "room": "my_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:14:03+02:00", + "mac": "A4:C1:38:DD:9F:EB", + "temperature": 20.2, + "humidity": 70, + "battery_percent": 85, + "battery_volt": 2.975, + "count": 69 + }, + "device": { + "mac": "A4:C1:38:DD:9F:EB", + "name": "5", + "room": "kitchen" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:14:03+02:00", + "mac": "A4:C1:38:D0:78:41", + "temperature": 20.0, + "humidity": 64, + "battery_percent": 85, + "battery_volt": 2.975, + "count": 90 + }, + "device": { + "mac": "A4:C1:38:D0:78:41", + "name": "2", + "room": "pantry" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:14:07+02:00", + "mac": "A4:C1:38:BB:98:5C", + "temperature": 20.2, + "humidity": 71, + "battery_percent": 87, + "battery_volt": 2.99, + "count": 78 + }, + "device": { + "mac": "A4:C1:38:BB:98:5C", + "name": "3", + "room": "living_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:15:01+02:00", + "mac": "A4:C1:38:46:D7:75", + "temperature": 20.3, + "humidity": 69, + "battery_percent": 87, + "battery_volt": 2.984, + "count": 71 + }, + "device": { + "mac": "A4:C1:38:46:D7:75", + "name": "4", + "room": "dinner_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:15:01+02:00", + "mac": "A4:C1:38:D0:78:41", + "temperature": 20.0, + "humidity": 64, + "battery_percent": 85, + "battery_volt": 2.975, + "count": 91 + }, + "device": { + "mac": "A4:C1:38:D0:78:41", + "name": "2", + "room": "pantry" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:15:01+02:00", + "mac": "A4:C1:38:DD:9F:EB", + "temperature": 20.2, + "humidity": 70, + "battery_percent": 85, + "battery_volt": 2.975, + "count": 70 + }, + "device": { + "mac": "A4:C1:38:DD:9F:EB", + "name": "5", + "room": "kitchen" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:15:02+02:00", + "mac": "A4:C1:38:BB:98:5C", + "temperature": 20.2, + "humidity": 71, + "battery_percent": 87, + "battery_volt": 2.987, + "count": 79 + }, + "device": { + "mac": "A4:C1:38:BB:98:5C", + "name": "3", + "room": "living_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:15:02+02:00", + "mac": "A4:C1:38:9A:81:25", + "temperature": 25.1, + "humidity": 51, + "battery_percent": 82, + "battery_volt": 2.947, + "count": 229 + }, + "device": { + "mac": "A4:C1:38:9A:81:25", + "name": "6", + "room": "server" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:15:03+02:00", + "mac": "A4:C1:38:83:05:E8", + "temperature": 12.4, + "humidity": 78, + "battery_percent": 72, + "battery_volt": 2.862, + "count": 84 + }, + "device": { + "mac": "A4:C1:38:83:05:E8", + "name": "my_room", + "room": "my_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:16:01+02:00", + "mac": "A4:C1:38:46:D7:75", + "temperature": 20.3, + "humidity": 69, + "battery_percent": 87, + "battery_volt": 2.984, + "count": 72 + }, + "device": { + "mac": "A4:C1:38:46:D7:75", + "name": "4", + "room": "dinner_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:16:02+02:00", + "mac": "A4:C1:38:D0:78:41", + "temperature": 20.0, + "humidity": 64, + "battery_percent": 85, + "battery_volt": 2.975, + "count": 92 + }, + "device": { + "mac": "A4:C1:38:D0:78:41", + "name": "2", + "room": "pantry" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:16:03+02:00", + "mac": "A4:C1:38:9A:81:25", + "temperature": 25.1, + "humidity": 51, + "battery_percent": 82, + "battery_volt": 2.947, + "count": 230 + }, + "device": { + "mac": "A4:C1:38:9A:81:25", + "name": "6", + "room": "server" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:16:03+02:00", + "mac": "A4:C1:38:83:05:E8", + "temperature": 12.2, + "humidity": 79, + "battery_percent": 72, + "battery_volt": 2.862, + "count": 88 + }, + "device": { + "mac": "A4:C1:38:83:05:E8", + "name": "my_room", + "room": "my_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:16:04+02:00", + "mac": "A4:C1:38:DD:9F:EB", + "temperature": 20.2, + "humidity": 70, + "battery_percent": 85, + "battery_volt": 2.975, + "count": 72 + }, + "device": { + "mac": "A4:C1:38:DD:9F:EB", + "name": "5", + "room": "kitchen" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:16:18+02:00", + "mac": "A4:C1:38:BB:98:5C", + "temperature": 20.2, + "humidity": 71, + "battery_percent": 87, + "battery_volt": 2.987, + "count": 80 + }, + "device": { + "mac": "A4:C1:38:BB:98:5C", + "name": "3", + "room": "living_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:17:02+02:00", + "mac": "A4:C1:38:46:D7:75", + "temperature": 20.3, + "humidity": 69, + "battery_percent": 87, + "battery_volt": 2.984, + "count": 73 + }, + "device": { + "mac": "A4:C1:38:46:D7:75", + "name": "4", + "room": "dinner_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:17:02+02:00", + "mac": "A4:C1:38:D0:78:41", + "temperature": 20.0, + "humidity": 64, + "battery_percent": 85, + "battery_volt": 2.974, + "count": 93 + }, + "device": { + "mac": "A4:C1:38:D0:78:41", + "name": "2", + "room": "pantry" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:17:02+02:00", + "mac": "A4:C1:38:DD:9F:EB", + "temperature": 20.2, + "humidity": 70, + "battery_percent": 85, + "battery_volt": 2.975, + "count": 73 + }, + "device": { + "mac": "A4:C1:38:DD:9F:EB", + "name": "5", + "room": "kitchen" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:17:03+02:00", + "mac": "A4:C1:38:83:05:E8", + "temperature": 12.2, + "humidity": 79, + "battery_percent": 72, + "battery_volt": 2.861, "count": 92 }, "device": { @@ -145,29 +2257,13 @@ }, { "data": { - "timestamp": "2024-06-23T00:14:11+02:00", - "mac": "A4:C1:38:46:D7:75", - "temperature": 20.4, - "humidity": 69, - "battery_percent": 87, - "battery_volt": 2.986, - "count": 2 - }, - "device": { - "mac": "A4:C1:38:46:D7:75", - "name": "4", - "room": "dinner_room" - } - }, - { - "data": { - "timestamp": "2024-06-23T00:14:12+02:00", + "timestamp": "2024-06-23T01:17:05+02:00", "mac": "A4:C1:38:9A:81:25", "temperature": 25.1, "humidity": 51, "battery_percent": 82, - "battery_volt": 2.945, - "count": 160 + "battery_volt": 2.947, + "count": 232 }, "device": { "mac": "A4:C1:38:9A:81:25", @@ -177,18 +2273,306 @@ }, { "data": { - "timestamp": "2024-06-23T00:14:12+02:00", - "mac": "A4:C1:38:DD:9F:EB", - "temperature": 20.3, - "humidity": 70, + "timestamp": "2024-06-23T01:17:07+02:00", + "mac": "A4:C1:38:BB:98:5C", + "temperature": 20.2, + "humidity": 71, + "battery_percent": 87, + "battery_volt": 2.987, + "count": 81 + }, + "device": { + "mac": "A4:C1:38:BB:98:5C", + "name": "3", + "room": "living_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:18:01+02:00", + "mac": "A4:C1:38:D0:78:41", + "temperature": 20.0, + "humidity": 64, "battery_percent": 85, "battery_volt": 2.974, - "count": 1 + "count": 94 + }, + "device": { + "mac": "A4:C1:38:D0:78:41", + "name": "2", + "room": "pantry" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:18:01+02:00", + "mac": "A4:C1:38:DD:9F:EB", + "temperature": 20.2, + "humidity": 70, + "battery_percent": 85, + "battery_volt": 2.975, + "count": 74 }, "device": { "mac": "A4:C1:38:DD:9F:EB", "name": "5", "room": "kitchen" } + }, + { + "data": { + "timestamp": "2024-06-23T01:18:02+02:00", + "mac": "A4:C1:38:9A:81:25", + "temperature": 25.1, + "humidity": 51, + "battery_percent": 82, + "battery_volt": 2.947, + "count": 233 + }, + "device": { + "mac": "A4:C1:38:9A:81:25", + "name": "6", + "room": "server" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:18:02+02:00", + "mac": "A4:C1:38:83:05:E8", + "temperature": 12.2, + "humidity": 79, + "battery_percent": 72, + "battery_volt": 2.861, + "count": 96 + }, + "device": { + "mac": "A4:C1:38:83:05:E8", + "name": "my_room", + "room": "my_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:18:02+02:00", + "mac": "A4:C1:38:46:D7:75", + "temperature": 20.3, + "humidity": 69, + "battery_percent": 87, + "battery_volt": 2.986, + "count": 75 + }, + "device": { + "mac": "A4:C1:38:46:D7:75", + "name": "4", + "room": "dinner_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:18:04+02:00", + "mac": "A4:C1:38:BB:98:5C", + "temperature": 20.2, + "humidity": 71, + "battery_percent": 87, + "battery_volt": 2.988, + "count": 82 + }, + "device": { + "mac": "A4:C1:38:BB:98:5C", + "name": "3", + "room": "living_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:19:02+02:00", + "mac": "A4:C1:38:9A:81:25", + "temperature": 25.1, + "humidity": 51, + "battery_percent": 82, + "battery_volt": 2.947, + "count": 234 + }, + "device": { + "mac": "A4:C1:38:9A:81:25", + "name": "6", + "room": "server" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:19:02+02:00", + "mac": "A4:C1:38:83:05:E8", + "temperature": 12.1, + "humidity": 80, + "battery_percent": 72, + "battery_volt": 2.861, + "count": 100 + }, + "device": { + "mac": "A4:C1:38:83:05:E8", + "name": "my_room", + "room": "my_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:19:03+02:00", + "mac": "A4:C1:38:DD:9F:EB", + "temperature": 20.2, + "humidity": 70, + "battery_percent": 85, + "battery_volt": 2.974, + "count": 75 + }, + "device": { + "mac": "A4:C1:38:DD:9F:EB", + "name": "5", + "room": "kitchen" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:19:05+02:00", + "mac": "A4:C1:38:46:D7:75", + "temperature": 20.3, + "humidity": 69, + "battery_percent": 87, + "battery_volt": 2.986, + "count": 76 + }, + "device": { + "mac": "A4:C1:38:46:D7:75", + "name": "4", + "room": "dinner_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:19:05+02:00", + "mac": "A4:C1:38:D0:78:41", + "temperature": 20.0, + "humidity": 64, + "battery_percent": 85, + "battery_volt": 2.974, + "count": 95 + }, + "device": { + "mac": "A4:C1:38:D0:78:41", + "name": "2", + "room": "pantry" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:19:08+02:00", + "mac": "A4:C1:38:BB:98:5C", + "temperature": 20.1, + "humidity": 71, + "battery_percent": 87, + "battery_volt": 2.988, + "count": 84 + }, + "device": { + "mac": "A4:C1:38:BB:98:5C", + "name": "3", + "room": "living_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:20:02+02:00", + "mac": "A4:C1:38:46:D7:75", + "temperature": 20.3, + "humidity": 69, + "battery_percent": 87, + "battery_volt": 2.985, + "count": 77 + }, + "device": { + "mac": "A4:C1:38:46:D7:75", + "name": "4", + "room": "dinner_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:20:02+02:00", + "mac": "A4:C1:38:DD:9F:EB", + "temperature": 20.2, + "humidity": 70, + "battery_percent": 85, + "battery_volt": 2.974, + "count": 76 + }, + "device": { + "mac": "A4:C1:38:DD:9F:EB", + "name": "5", + "room": "kitchen" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:20:03+02:00", + "mac": "A4:C1:38:83:05:E8", + "temperature": 12.0, + "humidity": 80, + "battery_percent": 72, + "battery_volt": 2.858, + "count": 104 + }, + "device": { + "mac": "A4:C1:38:83:05:E8", + "name": "my_room", + "room": "my_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:20:05+02:00", + "mac": "A4:C1:38:BB:98:5C", + "temperature": 20.1, + "humidity": 71, + "battery_percent": 87, + "battery_volt": 2.988, + "count": 85 + }, + "device": { + "mac": "A4:C1:38:BB:98:5C", + "name": "3", + "room": "living_room" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:20:05+02:00", + "mac": "A4:C1:38:9A:81:25", + "temperature": 25.1, + "humidity": 51, + "battery_percent": 82, + "battery_volt": 2.947, + "count": 235 + }, + "device": { + "mac": "A4:C1:38:9A:81:25", + "name": "6", + "room": "server" + } + }, + { + "data": { + "timestamp": "2024-06-23T01:20:06+02:00", + "mac": "A4:C1:38:D0:78:41", + "temperature": 20.0, + "humidity": 64, + "battery_percent": 86, + "battery_volt": 2.976, + "count": 96 + }, + "device": { + "mac": "A4:C1:38:D0:78:41", + "name": "2", + "room": "pantry" + } } -] \ No newline at end of file +] diff --git a/Charts/index.html b/Charts/index.html index 1ff615a..8e600f5 100644 --- a/Charts/index.html +++ b/Charts/index.html @@ -25,30 +25,88 @@ .device-info { margin-bottom: 10px; } + .datetimepicker-container { + width: 80%; + margin: 20px auto; + display: flex; + justify-content: space-between; + } + .datetimepicker-container input, .datetimepicker-container button { + padding: 10px; + font-size: 16px; + background-color: #1e1e1e; + border: 1px solid #444; + border-radius: 5px; + color: #ffffff; + } + .datetimepicker-container button { + cursor: pointer; + } - + +

Geräte-Diagramme

+
+ + + + + +
diff --git a/Charts/script.js b/Charts/script.js deleted file mode 100644 index 2ba5ddc..0000000 --- a/Charts/script.js +++ /dev/null @@ -1,107 +0,0 @@ -document.addEventListener('DOMContentLoaded', function () { - fetch('data.json') - .then(response => { - if (!response.ok) { - throw new Error('Network response was not ok ' + response.statusText); - } - return response.json(); - }) - .then(data => { - const devices = {}; - - data.forEach(entry => { - const mac = entry.device.mac; - if (!devices[mac]) { - devices[mac] = { - temperatureData: [], - humidityData: [], - labels: [], - name: entry.device.name, - room: entry.device.room - }; - } - devices[mac].labels.push(new Date(entry.data.timestamp)); - devices[mac].temperatureData.push(entry.data.temperature); - devices[mac].humidityData.push(entry.data.humidity); - }); - - const chartsContainer = document.getElementById('chartsContainer'); - - Object.keys(devices).forEach(mac => { - const device = devices[mac]; - - const chartContainer = document.createElement('div'); - chartContainer.className = 'chart-container'; - - const canvas = document.createElement('canvas'); - canvas.id = `chart-${mac}`; - - const title = document.createElement('h3'); - title.textContent = `Device ${device.name} in ${device.room}`; - - chartContainer.appendChild(title); - chartContainer.appendChild(canvas); - chartsContainer.appendChild(chartContainer); - - const ctx = canvas.getContext('2d'); - new Chart(ctx, { - type: 'line', - data: { - labels: device.labels, - datasets: [ - { - label: 'Temperatur (°C)', - data: device.temperatureData, - borderColor: 'rgba(255, 99, 132, 1)', - backgroundColor: 'rgba(255, 99, 132, 0.2)', - borderWidth: 1, - yAxisID: 'y1' - }, - { - label: 'Luftfeuchtigkeit (%)', - data: device.humidityData, - borderColor: 'rgba(54, 162, 235, 1)', - backgroundColor: 'rgba(54, 162, 235, 0.2)', - borderWidth: 1, - yAxisID: 'y2' - } - ] - }, - options: { - responsive: true, - scales: { - x: { - type: 'time', - time: { - unit: 'minute', - tooltipFormat: 'll HH:mm' - } - }, - y1: { - type: 'linear', - position: 'left', - beginAtZero: true, - title: { - display: true, - text: 'Temperatur (°C)' - } - }, - y2: { - type: 'linear', - position: 'right', - beginAtZero: true, - title: { - display: true, - text: 'Luftfeuchtigkeit (%)' - }, - grid: { - drawOnChartArea: false - } - } - } - } - }); - }); - }) - .catch(error => console.error('Error loading JSON data:', error)); -}); diff --git a/Charts/styles.css b/Charts/styles.css deleted file mode 100644 index 9cfcc97..0000000 --- a/Charts/styles.css +++ /dev/null @@ -1,20 +0,0 @@ -body { - font-family: Arial, sans-serif; - display: flex; - flex-direction: column; - align-items: center; - background-color: #f4f4f4; - margin: 0; - padding: 20px; -} - -.chart-container { - width: 80%; - margin: 20px 0; -} - -canvas { - background-color: #ffffff; - border-radius: 8px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} diff --git a/Dockerfile b/Dockerfile index 0a9db2a..2d972d4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,27 +1,35 @@ +FROM python:3.12-alpine3.20 AS build_bluepy + +RUN apk add \ + bluez \ + make \ + git \glib-dev \ + gcc \ + build-base \ + freetype-dev \ + libpng-dev \ + openblas-dev + +RUN git clone https://github.com/IanHarvey/bluepy.git && \ + cd bluepy && \ + python3.12 setup.py build && \ + python3.12 setup.py install + FROM python:3.12-alpine3.20 WORKDIR = /src - COPY python/src/ . COPY python/requierements.txt . COPY python/docker_entrypoint.sh / - RUN mkdir data +VOLUME data -# RUN apt-get update && \ -# apt-get install -y bluez sudo +RUN apk add sudo bluez +# Copy bluepy from the bluepy build stage +COPY --from=build_bluepy /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages +COPY --from=build_bluepy /usr/local/bin /usr/local/bin -RUN apk add --no-cache \ - sudo \ - make \ - bluez \ - bluez-deprecated \ - alsa-utils \ - alsa-utils-doc \ - alsa-lib \ - alsaconf - -RUN pip3.12 install -r requierements.txt && rm -f requierements.txt +RUN pip3.12 install -r requierements.txt && rm requierements.txt ENTRYPOINT sh /docker_entrypoint.sh \ No newline at end of file diff --git a/README.md b/README.md index 2675b4a..3215cef 100644 --- a/README.md +++ b/README.md @@ -63,3 +63,8 @@ sudo sh run_docker.sh ### MicroPython for MicroController Coming when I develop it... + + +# Resources +- https://pythonspeed.com/articles/alpine-docker-python this article is nuts :D +- https://docs.docker.com/build/building/multi-stage/ \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 2f47bf8..71086ff 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,6 +2,6 @@ version: '3' services: atc_mithermometer_gateway: - image: atc-mithermometer-gateway:develop + image: dasmoorhuhn/atc-mithermometer-gateway:develop-alpine container_name: ATC_MiThermometer_Gateway build: . \ No newline at end of file diff --git a/log_to_json.py b/log_to_json.py deleted file mode 100644 index 7cef3ea..0000000 --- a/log_to_json.py +++ /dev/null @@ -1,13 +0,0 @@ -import json - - -with open('history.txt', 'r') as file: - content = file.readlines() - -lines = [] -for line in content: - line = json.loads(line.strip()) - lines.append(line) - -with open('history.json', 'w') as file: - file.write(json.dumps(lines)) diff --git a/python/docker_entrypoint.sh b/python/docker_entrypoint.sh index b4a9ea2..f672995 100644 --- a/python/docker_entrypoint.sh +++ b/python/docker_entrypoint.sh @@ -1,8 +1,8 @@ #!/bin/bash -service dbus start -bluetoothd & +# service dbus start +# bluetoothd & -/bin/bash +/bin/sh sudo python3 main.py diff --git a/python/requierements.txt b/python/requierements.txt index dd13488..3d4fa3d 100644 --- a/python/requierements.txt +++ b/python/requierements.txt @@ -1,4 +1,3 @@ -bluepy pyyaml bs4 lxml diff --git a/python/src/log_data.py b/python/src/log_data.py index 2724848..b2a5589 100644 --- a/python/src/log_data.py +++ b/python/src/log_data.py @@ -6,21 +6,30 @@ from devices import Device workdir, filename = os.path.split(os.path.abspath(__file__)) -def log_to_txt(devices): - with open(f'{workdir}{os.sep}history.json', 'r') as file: - data = json.load(file) +def log_to_json(devices): + for device in devices: + dev, data_obj, from_config = device + data_obj: Data + from_config: Device + file_name = f'{workdir}{os.sep}data{os.sep}{str(data_obj.mac).replace(":", "-")}.json' - with open(f'{workdir}{os.sep}history.json', 'w') as file: - for device in devices: - dev, data_obj, from_config = device - data_obj:Data - from_config:Device - data.append({ - "data": data_obj.to_json(), - "device": from_config.to_json() - }) - final_data = {"measurements": data} - file.write(json.dumps(data, indent=2)) + try: + with open(file_name, 'r') as file: data = json.load(file) + except: + with open(file_name, 'w') as file: file.write("[]") + data = [] + + data.append({ + "timestamp": data_obj.timestamp, + "temperature": data_obj.temperature, + "humidity": data_obj.humidity, + "battery_percent": data_obj.battery_percent, + "battery_volt": data_obj.battery_volt, + "name": from_config.name, + "room": from_config.room + }) + + with open(file_name, 'w') as file: file.write(json.dumps(data, indent=2)) def log_to_mongodb(data): diff --git a/python/src/main.py b/python/src/main.py index 420c42b..c993e3d 100644 --- a/python/src/main.py +++ b/python/src/main.py @@ -1,7 +1,7 @@ from discovery import start_discovery from loop import start_loop -from log_data import log_to_txt +from log_data import log_to_json devices = start_discovery() -log_to_txt(devices) +log_to_json(devices) diff --git a/run_docker.sh b/run_docker.sh index 20f4df3..eae75ca 100644 --- a/run_docker.sh +++ b/run_docker.sh @@ -1,9 +1,12 @@ +TAG=develop-alpine +CONTAINER=dasmoorhuhn/atc-mithermometer-gateway:$TAG + sudo killall -9 bluetoothd -docker stop atc-mithermometer-gateway:develop +docker stop $CONTAINER > /dev/null 2>&1 docker run \ --cap-add=SYS_ADMIN \ --cap-add=NET_ADMIN \ --net=host \ -v /var/run/dbus/:/var/run/dbus/ \ - -v /path/to/data:/src/data \ - atc-mithermometer-gateway:develop \ No newline at end of file + -v ./data:/src/data \ + $CONTAINER \ No newline at end of file From 345666cb2eca8b55040d0aa2103100099506dbb6 Mon Sep 17 00:00:00 2001 From: DasMoorhuhn Date: Thu, 27 Jun 2024 04:02:36 +0200 Subject: [PATCH 06/10] charts --- Dockerfile | 20 +++--- docker-compose.yml | 2 +- python/docker_entrypoint.sh | 10 +-- python/requierements.txt | 4 +- python/src/api_endpoints.py | 39 +++++++++++ python/src/chart.html | 128 ++++++++++++++++++++++++++++++++++++ python/src/log_data.py | 8 ++- python/src/loop.py | 5 +- python/src/main.py | 8 ++- run_docker.sh | 15 ++++- stop_docker.sh | 3 + 11 files changed, 213 insertions(+), 29 deletions(-) create mode 100644 python/src/api_endpoints.py create mode 100644 python/src/chart.html create mode 100644 stop_docker.sh diff --git a/Dockerfile b/Dockerfile index 2d972d4..2803780 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,9 @@ FROM python:3.12-alpine3.20 AS build_bluepy RUN apk add \ - bluez \ make \ - git \glib-dev \ + git \ + glib-dev \ gcc \ build-base \ freetype-dev \ @@ -16,20 +16,22 @@ RUN git clone https://github.com/IanHarvey/bluepy.git && \ python3.12 setup.py install FROM python:3.12-alpine3.20 - -WORKDIR = /src -COPY python/src/ . -COPY python/requierements.txt . -COPY python/docker_entrypoint.sh / +WORKDIR /src +COPY ./python/src/ . +COPY ./python/requierements.txt . +COPY ./python/docker_entrypoint.sh / RUN mkdir data -VOLUME data +RUN touch DOCKER +VOLUME /src/data -RUN apk add sudo bluez +RUN apk add --no-cache sudo bluez tzdata +ENV TZ=Europe/Berlin # Copy bluepy from the bluepy build stage COPY --from=build_bluepy /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages COPY --from=build_bluepy /usr/local/bin /usr/local/bin RUN pip3.12 install -r requierements.txt && rm requierements.txt +# RUN echo '@reboot root python3.12 /src/serve_json.py' >> /etc/crontab ENTRYPOINT sh /docker_entrypoint.sh \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 71086ff..0a3b958 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,5 +3,5 @@ version: '3' services: atc_mithermometer_gateway: image: dasmoorhuhn/atc-mithermometer-gateway:develop-alpine - container_name: ATC_MiThermometer_Gateway + container_name: ATC_MiThermometer_Gateway_Build build: . \ No newline at end of file diff --git a/python/docker_entrypoint.sh b/python/docker_entrypoint.sh index f672995..c9ddadd 100644 --- a/python/docker_entrypoint.sh +++ b/python/docker_entrypoint.sh @@ -1,8 +1,4 @@ -#!/bin/bash +#!/bin/sh -# service dbus start -# bluetoothd & - -/bin/sh - -sudo python3 main.py +python3.12 api_endpoints.py & +sudo python3.12 main.py \ No newline at end of file diff --git a/python/requierements.txt b/python/requierements.txt index 3d4fa3d..0965033 100644 --- a/python/requierements.txt +++ b/python/requierements.txt @@ -1,4 +1,6 @@ pyyaml bs4 lxml -requests \ No newline at end of file +requests +flask +flask_cors \ No newline at end of file diff --git a/python/src/api_endpoints.py b/python/src/api_endpoints.py new file mode 100644 index 0000000..6448f3b --- /dev/null +++ b/python/src/api_endpoints.py @@ -0,0 +1,39 @@ +import os +from flask import Flask +from flask import jsonify +from flask import send_from_directory +from flask_cors import CORS +from flask_cors import cross_origin + + +class API: + """ + API endpoints + """ + + def __init__(self): + self.app = Flask(import_name='backend', static_folder='/data', static_url_path='') + CORS(self.app) + self.app.config['CORS_HEADERS'] = 'Content-Type' + + # --------Static Routes------- + @self.app.route('/charts') + @cross_origin() + def serve_index(): + return send_from_directory('/src', 'chart.html') + + @self.app.route('/json') + @cross_origin() + def serve_get_list_of_json(): + workdir, filename = os.path.split(os.path.abspath(__file__)) + return jsonify(os.listdir(f'{workdir}/data')) + + @self.app.route('/json/') + @cross_origin() + def serve_json(path): + workdir, filename = os.path.split(os.path.abspath(__file__)) + return send_from_directory(f'{workdir}/data', path) + + +api = API() +api.app.run(host='0.0.0.0', port=8000) diff --git a/python/src/chart.html b/python/src/chart.html new file mode 100644 index 0000000..1f6bc71 --- /dev/null +++ b/python/src/chart.html @@ -0,0 +1,128 @@ + + + + + + Messdaten Charts + + + + + +

Messdaten Charts

+
+ + + + diff --git a/python/src/log_data.py b/python/src/log_data.py index b2a5589..54d541a 100644 --- a/python/src/log_data.py +++ b/python/src/log_data.py @@ -1,17 +1,19 @@ import os +import sys import json from data_class import Data from devices import Device -workdir, filename = os.path.split(os.path.abspath(__file__)) - def log_to_json(devices): + workdir, filename = os.path.split(os.path.abspath(__file__)) + for device in devices: dev, data_obj, from_config = device data_obj: Data from_config: Device - file_name = f'{workdir}{os.sep}data{os.sep}{str(data_obj.mac).replace(":", "-")}.json' + file_name = f'{workdir}/data/{str(data_obj.mac).replace(":", "-")}.json' + print(file_name) try: with open(file_name, 'r') as file: data = json.load(file) diff --git a/python/src/loop.py b/python/src/loop.py index e50ca33..df71b86 100644 --- a/python/src/loop.py +++ b/python/src/loop.py @@ -1,9 +1,10 @@ from time import sleep - +from log_data import log_to_json from discovery import start_discovery def start_loop(interval=60): while True: - start_discovery() + devices = start_discovery() + log_to_json(devices) sleep(interval) diff --git a/python/src/main.py b/python/src/main.py index c993e3d..79b9328 100644 --- a/python/src/main.py +++ b/python/src/main.py @@ -1,7 +1,9 @@ from discovery import start_discovery -from loop import start_loop from log_data import log_to_json +from loop import start_loop -devices = start_discovery() -log_to_json(devices) +# devices = start_discovery() +# log_to_json(devices) + +start_loop() diff --git a/run_docker.sh b/run_docker.sh index eae75ca..54837dd 100644 --- a/run_docker.sh +++ b/run_docker.sh @@ -1,12 +1,21 @@ TAG=develop-alpine CONTAINER=dasmoorhuhn/atc-mithermometer-gateway:$TAG -sudo killall -9 bluetoothd -docker stop $CONTAINER > /dev/null 2>&1 +sudo killall -9 bluetoothd > /dev/null 2>&1 +# docker stop $CONTAINER > /dev/null 2>&1 +sh stop_docker.sh +docker container rm ATC_MiThermometer_Gateway > /dev/null 2>&1 +echo Start container... docker run \ --cap-add=SYS_ADMIN \ --cap-add=NET_ADMIN \ --net=host \ + --name=ATC_MiThermometer_Gateway \ + --restart unless-stopped \ + --tty \ + -ti \ -v /var/run/dbus/:/var/run/dbus/ \ -v ./data:/src/data \ - $CONTAINER \ No newline at end of file + $CONTAINER \ + +docker container rm ATC_MiThermometer_Gateway > /dev/null 2>&1 \ No newline at end of file diff --git a/stop_docker.sh b/stop_docker.sh new file mode 100644 index 0000000..120d2ca --- /dev/null +++ b/stop_docker.sh @@ -0,0 +1,3 @@ +echo Stopping container gracefully... +docker stop ATC_MiThermometer_Gateway +echo Done \ No newline at end of file From 5029e46359f3e34a7220c3c94163d84d56988b4d Mon Sep 17 00:00:00 2001 From: DasMoorhuhn Date: Sat, 29 Jun 2024 03:32:54 +0200 Subject: [PATCH 07/10] multiplatform build --- .gitignore | 4 +- Dockerfile | 38 +++++++--- build_docker.sh | 35 +++++++++ build_docker_multi_platforn.sh | 10 +++ python/docker_entrypoint.sh | 8 +- python/requierements.txt | 1 - python/src/discovery.py | 2 +- python/src/load_env.py | 16 ++++ python/src/loop.py | 5 +- python/src/main.py | 22 +++++- run_docker.sh | 132 ++++++++++++++++++++++++++++----- 11 files changed, 235 insertions(+), 38 deletions(-) create mode 100644 build_docker.sh create mode 100644 build_docker_multi_platforn.sh create mode 100644 python/src/load_env.py diff --git a/.gitignore b/.gitignore index 48b6f47..73308d2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ __pycache__ devices.yml history.* -data/ \ No newline at end of file +data/ +*.iso +*.cow \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 2803780..3fb8851 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,7 @@ -FROM python:3.12-alpine3.20 AS build_bluepy +# This is the build stage for getting all python libs. Mainly it's for reducing the final image size because for building and installing the pips, many tools are needed which are just bloat to the final image. +FROM python:3.12-alpine3.20 AS pip_build_stage + +COPY ./python/requierements.txt / RUN apk add \ make \ @@ -8,17 +11,32 @@ RUN apk add \ build-base \ freetype-dev \ libpng-dev \ - openblas-dev + openblas-dev \ + libxml2-dev \ + libxslt-dev +# BluePy needs to be build here... idk why but pip install fails on alpine somehow RUN git clone https://github.com/IanHarvey/bluepy.git && \ cd bluepy && \ python3.12 setup.py build && \ - python3.12 setup.py install + python3.12 setup.py install && \ + cd / +# Normal pip install for pyyaml failed on RPI4, so I build it here +RUN git clone https://github.com/yaml/pyyaml.git && \ + cd pyyaml && \ + python3.12 setup.py build && \ + python3.12 setup.py install && \ + cd / + +RUN pip3.12 install -r requierements.txt + + +# This is the stage for the final image FROM python:3.12-alpine3.20 + WORKDIR /src COPY ./python/src/ . -COPY ./python/requierements.txt . COPY ./python/docker_entrypoint.sh / RUN mkdir data RUN touch DOCKER @@ -26,12 +44,10 @@ VOLUME /src/data RUN apk add --no-cache sudo bluez tzdata ENV TZ=Europe/Berlin +ENV DOCKER=TRUE -# Copy bluepy from the bluepy build stage -COPY --from=build_bluepy /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages -COPY --from=build_bluepy /usr/local/bin /usr/local/bin +# Copy pips from the pip build stage +COPY --from=pip_build_stage /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages +COPY --from=pip_build_stage /usr/local/bin /usr/local/bin -RUN pip3.12 install -r requierements.txt && rm requierements.txt -# RUN echo '@reboot root python3.12 /src/serve_json.py' >> /etc/crontab - -ENTRYPOINT sh /docker_entrypoint.sh \ No newline at end of file +ENTRYPOINT sh /docker_entrypoint.sh diff --git a/build_docker.sh b/build_docker.sh new file mode 100644 index 0000000..f06bd74 --- /dev/null +++ b/build_docker.sh @@ -0,0 +1,35 @@ +ARCH=linux/arm/v6 +#ARCH=linux/amd64 +TAG=develop +HELP="USAGE: sh build_docker.sh \n +[ -a | --architecture ] Select a architecture. Default is auto\n +[ -t | --tag ] Set a docker tag. Default is develop \n +[ -h | --help ] Get this dialog" + +docker_build(){ + docker build --build-arg TARGETPLATFORM=$ARCH --platform $ARCH --tag dasmoorhuhn/atc-mithermometer-gateway:$TAG . +} + +while [ "$1" != "" ]; do + case $1 in + -a | --architecture ) + shift + ARCH=$1 + shift + ;; + -t | --tag ) + shift + TAG=$1 + shift + ;; + -h | --help ) + echo $HELP + exit + ;; + * ) + echo $HELP + exit 1 + esac +done + +docker_build \ No newline at end of file diff --git a/build_docker_multi_platforn.sh b/build_docker_multi_platforn.sh new file mode 100644 index 0000000..5a36005 --- /dev/null +++ b/build_docker_multi_platforn.sh @@ -0,0 +1,10 @@ +TAG=develop + +set e +docker buildx version +unset e + +docker buildx create --name builder +docker buildx use builder + +docker buildx build --tag dasmoorhuhn/atc-mithermometer-gateway:$TAG --platform=linux/amd64,linux/arm64,linux/arm --push . \ No newline at end of file diff --git a/python/docker_entrypoint.sh b/python/docker_entrypoint.sh index c9ddadd..9e4f582 100644 --- a/python/docker_entrypoint.sh +++ b/python/docker_entrypoint.sh @@ -1,4 +1,10 @@ #!/bin/sh -python3.12 api_endpoints.py & +env > .env + +if [ "$API" = true ]; then + python3.12 api_endpoints.py & + sleep 1 +fi + sudo python3.12 main.py \ No newline at end of file diff --git a/python/requierements.txt b/python/requierements.txt index 0965033..bfe7725 100644 --- a/python/requierements.txt +++ b/python/requierements.txt @@ -1,6 +1,5 @@ pyyaml bs4 -lxml requests flask flask_cors \ No newline at end of file diff --git a/python/src/discovery.py b/python/src/discovery.py index 0b7451b..7d8b17f 100644 --- a/python/src/discovery.py +++ b/python/src/discovery.py @@ -49,7 +49,7 @@ def cleanup(): devices = [] -def start_discovery(timeout=20.0): +def start_discovery(timeout=20): cleanup() global devices print(f'Start discovery with timout {timeout}s...') diff --git a/python/src/load_env.py b/python/src/load_env.py new file mode 100644 index 0000000..641c4fa --- /dev/null +++ b/python/src/load_env.py @@ -0,0 +1,16 @@ +# This is a quick and dirty hack since the ENVs from the docker run command won't show up in the main.py +# I echo the output from the env command of a .env file, read and load it here into the os.environ. +# https://stackoverflow.com/questions/78684481/python-wont-find-the-env-in-my-docker-container + + +import os + + +def load_env(): + if 'DOCKER' not in os.listdir('.'): return False + with open(file='.env', mode='r') as file: ENV = file.readlines() + for env in ENV: + env = env.strip() + key, value = env.split('=') + os.environ[key] = value + return True diff --git a/python/src/loop.py b/python/src/loop.py index df71b86..561a624 100644 --- a/python/src/loop.py +++ b/python/src/loop.py @@ -3,8 +3,9 @@ from log_data import log_to_json from discovery import start_discovery -def start_loop(interval=60): +def start_loop(interval=40, timeout=20): + print(f"Starting loop with interval {interval}s") while True: - devices = start_discovery() + devices = start_discovery(timeout=timeout) log_to_json(devices) sleep(interval) diff --git a/python/src/main.py b/python/src/main.py index 79b9328..934f877 100644 --- a/python/src/main.py +++ b/python/src/main.py @@ -1,9 +1,25 @@ +import os from discovery import start_discovery from log_data import log_to_json from loop import start_loop +from load_env import load_env +DOCKER = load_env() +INTERVAL = 40 +TIMEOUT = 20 -# devices = start_discovery() -# log_to_json(devices) +if DOCKER: + print("Running in docker") + interval = os.getenv('LOOP') + timeout = os.getenv('TIMEOUT') -start_loop() + try:INTERVAL = int(interval) + except:pass + + try:TIMEOUT = int(timeout) + except:pass + + start_loop(INTERVAL, TIMEOUT) + +else: + start_loop(interval=40) diff --git a/run_docker.sh b/run_docker.sh index 54837dd..c6e4b24 100644 --- a/run_docker.sh +++ b/run_docker.sh @@ -1,21 +1,117 @@ -TAG=develop-alpine +TAG=develop CONTAINER=dasmoorhuhn/atc-mithermometer-gateway:$TAG +CONTAINER_NAME=ATC_MiThermometer_Gateway +VOLUME=YOUR_VOLUME -sudo killall -9 bluetoothd > /dev/null 2>&1 -# docker stop $CONTAINER > /dev/null 2>&1 -sh stop_docker.sh -docker container rm ATC_MiThermometer_Gateway > /dev/null 2>&1 -echo Start container... -docker run \ - --cap-add=SYS_ADMIN \ - --cap-add=NET_ADMIN \ - --net=host \ - --name=ATC_MiThermometer_Gateway \ - --restart unless-stopped \ - --tty \ - -ti \ - -v /var/run/dbus/:/var/run/dbus/ \ - -v ./data:/src/data \ - $CONTAINER \ +D="" +TIME_ZONE="Europe/Berlin" +INTERACTIVE=false +BUILD=false +API=false +LOOP="0" +TIMEOUT="0" + +HELP="USAGE: sh run_docker.sh [OPTIONS] \n +[ -d ] Run in Backgrund \n +[ -t | --tag ] Set a docker tag \n +[ -b | --build ] Build the image before running the container \n +[ -l | --loop ] Start the gateway in looping mode. e.g.: --loop 40 will set the interval of the loop to 40s. Default is single run mode \n +[ -a | --api ] Start with the API \n +[ -tz | --timezone ] Set the timezone. Default is Europe/Berlin \n +[ -to | --timeout ] Set the timeout for the bluetooth scan. default is 20s \n +[ -h | --help ] Get this dialog" + +docker_run() { + sudo killall -9 bluetoothd > /dev/null 2>&1 + echo Killing old container... + docker stop $CONTAINER_NAME + docker container rm $CONTAINER_NAME > /dev/null 2>&1 + + COMMAND="docker run $D" + COMMAND="$COMMAND --cap-add=SYS_ADMIN" + COMMAND="$COMMAND --cap-add=NET_ADMIN" + COMMAND="$COMMAND --net=host" + COMMAND="$COMMAND --env TZ=$TIME_ZONE" + COMMAND="$COMMAND --env API=$API" + COMMAND="$COMMAND --name=$CONTAINER_NAME" + COMMAND="$COMMAND --restart=on-failure" + COMMAND="$COMMAND --volume=/var/run/dbus/:/var/run/dbus/" + COMMAND="$COMMAND --volume=$VOLUME:/src/data" + + if [ "$INTERACTIVE" = true ]; then + COMMAND="$COMMAND --interactive" + COMMAND="$COMMAND --tty" + COMMAND="$COMMAND --attach=stdout" + COMMAND="$COMMAND --attach=stdin" + fi + + if [ "$LOOP" != "0" ]; then + COMMAND="$COMMAND --env LOOP=$LOOP" + fi + + if [ "$TIMEOUT" != "0" ]; then + COMMAND="$COMMAND --env TIMEOUT=$TIMEOUT" + fi + + if [ "$BUILD" = true ]; then + sh build_docker.sh --tag $TAG + fi + + echo $COMMAND + echo Start container... + echo + $COMMAND $CONTAINER + + docker container rm $CONTAINER_NAME > /dev/null 2>&1 +} + +while [ "$1" != "" ]; do + case $1 in + -d ) + D="-d" + shift + ;; + -a | --api) + API=true + shift + ;; + -b | --build ) + BUILD=true + shift + ;; + -tz | --timezone ) + shift + TIME_ZONE=$1 + shift + ;; + -to | --timeout ) + shift + TIMEOUT=$1 + shift + ;; + -t | --tag ) + shift + TAG=$1 + shift + ;; + -l | --loop ) + shift + LOOP=$1 + shift + ;; + -i | --interactive ) + INTERACTIVE=true + shift + ;; + -h | --help ) + echo $HELP + exit + ;; + * ) + echo $HELP + exit 1 + esac +done + +docker_run -docker container rm ATC_MiThermometer_Gateway > /dev/null 2>&1 \ No newline at end of file From 4e76c7db1a991739ae43de379206e76fafffe8d3 Mon Sep 17 00:00:00 2001 From: DasMoorhuhn Date: Sat, 29 Jun 2024 04:12:26 +0200 Subject: [PATCH 08/10] docs --- Charts/index_2.html | 181 ++++++++++++++++++++++++++++++++++++++++++++ README.md | 43 ++++++++++- build_docker.sh | 19 +++-- 3 files changed, 229 insertions(+), 14 deletions(-) create mode 100644 Charts/index_2.html diff --git a/Charts/index_2.html b/Charts/index_2.html new file mode 100644 index 0000000..687204e --- /dev/null +++ b/Charts/index_2.html @@ -0,0 +1,181 @@ + + + + + + Geräte-Diagramme + + + + +

Geräte-Diagramme

+
+ + + +
+
+ + + + diff --git a/README.md b/README.md index 3215cef..602cfbf 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,13 @@ + +* [ATC_MiThermometer_Gateway](#atc_mithermometer_gateway) + * [Getting started](#getting-started) + * [Run Gateway](#run-gateway) + * [Shell Scripts](#shell-scripts) + * [Docker](#docker) + * [MicroPython for MicroController](#micropython-for-microcontroller) +* [Resources](#resources) + + # ATC_MiThermometer_Gateway Python gateway for the [custom firmware](https://github.com/atc1441/ATC_MiThermometer) for the [Xiaomi Thermometer LYWSD03MMC](https://www.mi.com/de/product/mi-temperature-and-humidity-monitor-2/). @@ -49,18 +59,43 @@ cd python/src sudo python3 main.py ``` -### Docker +## Shell Scripts -Build docker container (Currently broken) +**build_docker.sh** + +| Arg | Meaning | Default | +|---------------|----------------------------|---------------------------------------| +| -t \| --tag | Set a tag for build | develop | +| -i \| --image | Set a image name for build | dasmoorhuhn/atc-mithermometer-gateway | +| -h \| --help | Get this help in the CLI | | + +**run_docker.sh** + +| Arg | Meaning | Default | +|-------------------|----------------------------------------------|---------------| +| -d | Run in Backgrund | | +| -t \| --tag | Set a docker tag | develop | +| -b \| --build | Build the image before running the container | | +| -l \| --loop | Start the gateway in looping mode | | +| -a \| --api | Start with the API | false | +| -tz \| --timezone | Set the timezone | Europe/Berlin | +| -to \| --timeout | Set the timeout for the bluetooth scan | 20 | +| -h \| --help | Get this dialog in CLI | | + +## Docker + +Build docker container () ```bash -docker-compose build +sh build_docker.sh +# Or +sh build_docker.sh -i your-image-name -t your-tag ``` Run docker container. Killing the hosts bluetooth service is needed to access it from the docker container. ```bash sudo sh run_docker.sh ``` -### MicroPython for MicroController +## MicroPython for MicroController Coming when I develop it... diff --git a/build_docker.sh b/build_docker.sh index f06bd74..4e88e28 100644 --- a/build_docker.sh +++ b/build_docker.sh @@ -1,27 +1,26 @@ -ARCH=linux/arm/v6 -#ARCH=linux/amd64 TAG=develop +IMAGE=dasmoorhuhn/atc-mithermometer-gateway HELP="USAGE: sh build_docker.sh \n -[ -a | --architecture ] Select a architecture. Default is auto\n -[ -t | --tag ] Set a docker tag. Default is develop \n +[ -t | --tag ] Select a tag for building. Default is develop \n +[ -i | --image ] Select image tag for building. Default is dasmoorhuhn/atc-mithermometer-gateway \n [ -h | --help ] Get this dialog" docker_build(){ - docker build --build-arg TARGETPLATFORM=$ARCH --platform $ARCH --tag dasmoorhuhn/atc-mithermometer-gateway:$TAG . + docker build --tag $IMAGE:$TAG . } while [ "$1" != "" ]; do case $1 in - -a | --architecture ) - shift - ARCH=$1 - shift - ;; -t | --tag ) shift TAG=$1 shift ;; + -i | --image ) + shift + IMAGE=$1 + shift + ;; -h | --help ) echo $HELP exit From 285a2503d71f0016255a106ead9a9a8dd5153fa2 Mon Sep 17 00:00:00 2001 From: DasMoorhuhn Date: Wed, 3 Jul 2024 00:27:48 +0200 Subject: [PATCH 09/10] fix up some mess --- .media/demo.gif | Bin 0 -> 44099 bytes Dockerfile | 4 +-- README.md | 15 +++++------ python/docker_entrypoint.sh | 6 ++--- python/requierements.txt | 1 + python/src/load_env.py | 16 ------------ python/src/log_data.py | 4 ++- python/src/main.py | 20 +++++++++++---- run_docker.sh | 48 ++++++++++++++++++++++++++---------- 9 files changed, 67 insertions(+), 47 deletions(-) create mode 100644 .media/demo.gif delete mode 100644 python/src/load_env.py diff --git a/.media/demo.gif b/.media/demo.gif new file mode 100644 index 0000000000000000000000000000000000000000..5a550bceef0427aa30020e83840a71ead8407a84 GIT binary patch literal 44099 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 ij{ki3-KSAR%J0F;nJ literal 0 HcmV?d00001 diff --git a/Dockerfile b/Dockerfile index 3fb8851..bf958e5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -39,12 +39,12 @@ WORKDIR /src COPY ./python/src/ . COPY ./python/docker_entrypoint.sh / RUN mkdir data -RUN touch DOCKER VOLUME /src/data RUN apk add --no-cache sudo bluez tzdata ENV TZ=Europe/Berlin -ENV DOCKER=TRUE +ENV DOCKER=true +ENV API=false # Copy pips from the pip build stage COPY --from=pip_build_stage /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages diff --git a/README.md b/README.md index 602cfbf..a5e2f66 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ * [ATC_MiThermometer_Gateway](#atc_mithermometer_gateway) + * [Roadmap](#roadmap) * [Getting started](#getting-started) * [Run Gateway](#run-gateway) * [Shell Scripts](#shell-scripts) @@ -14,15 +15,16 @@ Python gateway for the [custom firmware](https://github.com/atc1441/ATC_MiThermo ![](.media/41N1IH9jwoL._AC_SL1024_.jpg) -**Features:** -- WIP +## Roadmap + +**Done:** +- Make in runnable in a docker container (because only cool people are using docker) +- Make docker image smaller. I mean shiiit 1GB D: should be possible to be under 500MB (It's now around 100MB) +- Implement a loop for fetching the data every X seconds **TODOs:** - [WIP] Can run on Raspberry Pi (3, 4, zero w) or any other Linux driven hardware which has BLE and WiFi support - [WIP] Storing temperature, humidity and battery state as json in a text file -- [WIP] Implement a loop for fetching the data every x minute -- [WIP] Make discoveries async -- [WIP] Make docker image smaller. I mean shiiit 1GB D: should be possible to be under 500MB - [TODO] Make a microPython version for using the raspberry pico w or any other microcontroller with BLE and WiFi support - [TODO] Collect data from multiple devices/gateways - [TODO] Command line tool for managing the devices @@ -31,11 +33,10 @@ Python gateway for the [custom firmware](https://github.com/atc1441/ATC_MiThermo - [TODO] MQTT publishing - [TODO] Maybe... a webinterface. But I suck at web stuff, so I don't know. - [TODO] Implement other BLE Sensors -- [BROK] Make in runnable in a docker container (because only cool people are using docker) **Current State** -![](.media/demo_001.gif) +![](.media/demo.gif) ## Getting started diff --git a/python/docker_entrypoint.sh b/python/docker_entrypoint.sh index 9e4f582..aeb780b 100644 --- a/python/docker_entrypoint.sh +++ b/python/docker_entrypoint.sh @@ -3,8 +3,8 @@ env > .env if [ "$API" = true ]; then - python3.12 api_endpoints.py & - sleep 1 + python3.12 api_endpoints.py & + sleep 1 fi -sudo python3.12 main.py \ No newline at end of file +python3.12 main.py \ No newline at end of file diff --git a/python/requierements.txt b/python/requierements.txt index bfe7725..b7ed443 100644 --- a/python/requierements.txt +++ b/python/requierements.txt @@ -1,3 +1,4 @@ +bluepy pyyaml bs4 requests diff --git a/python/src/load_env.py b/python/src/load_env.py deleted file mode 100644 index 641c4fa..0000000 --- a/python/src/load_env.py +++ /dev/null @@ -1,16 +0,0 @@ -# This is a quick and dirty hack since the ENVs from the docker run command won't show up in the main.py -# I echo the output from the env command of a .env file, read and load it here into the os.environ. -# https://stackoverflow.com/questions/78684481/python-wont-find-the-env-in-my-docker-container - - -import os - - -def load_env(): - if 'DOCKER' not in os.listdir('.'): return False - with open(file='.env', mode='r') as file: ENV = file.readlines() - for env in ENV: - env = env.strip() - key, value = env.split('=') - os.environ[key] = value - return True diff --git a/python/src/log_data.py b/python/src/log_data.py index 54d541a..a157481 100644 --- a/python/src/log_data.py +++ b/python/src/log_data.py @@ -4,6 +4,8 @@ import json from data_class import Data from devices import Device +DEBUG = True if os.getenv('DEBUG') == 'true' else False + def log_to_json(devices): workdir, filename = os.path.split(os.path.abspath(__file__)) @@ -13,7 +15,7 @@ def log_to_json(devices): data_obj: Data from_config: Device file_name = f'{workdir}/data/{str(data_obj.mac).replace(":", "-")}.json' - print(file_name) + print(file_name) if DEBUG else {} try: with open(file_name, 'r') as file: data = json.load(file) diff --git a/python/src/main.py b/python/src/main.py index 934f877..2fd502a 100644 --- a/python/src/main.py +++ b/python/src/main.py @@ -2,16 +2,25 @@ import os from discovery import start_discovery from log_data import log_to_json from loop import start_loop -from load_env import load_env -DOCKER = load_env() INTERVAL = 40 TIMEOUT = 20 +DOCKER = True if os.getenv('DOCKER') == 'true' else False +DEBUG = True if os.getenv('DEBUG') == 'true' else False +interval = os.getenv('LOOP') +timeout = os.getenv('TIMEOUT') + +if DEBUG: + print(f"INTERVAL: {INTERVAL}") + print(f"TIMEOUT: {TIMEOUT}") + print(f"interval: {interval}") + print(f"timeout: {timeout}") + print(f"DOCKER: {DOCKER}") + print(f"DEBUG: {DEBUG}") + print("") if DOCKER: print("Running in docker") - interval = os.getenv('LOOP') - timeout = os.getenv('TIMEOUT') try:INTERVAL = int(interval) except:pass @@ -19,7 +28,8 @@ if DOCKER: try:TIMEOUT = int(timeout) except:pass - start_loop(INTERVAL, TIMEOUT) + if interval is None: log_to_json(start_discovery(timeout=TIMEOUT)) + else:start_loop(INTERVAL, TIMEOUT) else: start_loop(interval=40) diff --git a/run_docker.sh b/run_docker.sh index c6e4b24..81ef62b 100644 --- a/run_docker.sh +++ b/run_docker.sh @@ -1,13 +1,14 @@ -TAG=develop -CONTAINER=dasmoorhuhn/atc-mithermometer-gateway:$TAG -CONTAINER_NAME=ATC_MiThermometer_Gateway +TAG="develop" +CONTAINER="dasmoorhuhn/atc-mithermometer-gateway" +CONTAINER_NAME="ATC_MiThermometer_Gateway" VOLUME=YOUR_VOLUME -D="" -TIME_ZONE="Europe/Berlin" +BACKGROUND="" +TIME_ZONE="" INTERACTIVE=false BUILD=false API=false +DEBUG=false LOOP="0" TIMEOUT="0" @@ -19,20 +20,19 @@ HELP="USAGE: sh run_docker.sh [OPTIONS] \n [ -a | --api ] Start with the API \n [ -tz | --timezone ] Set the timezone. Default is Europe/Berlin \n [ -to | --timeout ] Set the timeout for the bluetooth scan. default is 20s \n -[ -h | --help ] Get this dialog" +[ -h | --help ] Get this dialog \n +[ --debug ] Set into debug mode" docker_run() { sudo killall -9 bluetoothd > /dev/null 2>&1 echo Killing old container... - docker stop $CONTAINER_NAME + docker stop $CONTAINER_NAME > /dev/null 2>&1 docker container rm $CONTAINER_NAME > /dev/null 2>&1 - COMMAND="docker run $D" + COMMAND="docker run $BACKGROUND" COMMAND="$COMMAND --cap-add=SYS_ADMIN" COMMAND="$COMMAND --cap-add=NET_ADMIN" COMMAND="$COMMAND --net=host" - COMMAND="$COMMAND --env TZ=$TIME_ZONE" - COMMAND="$COMMAND --env API=$API" COMMAND="$COMMAND --name=$CONTAINER_NAME" COMMAND="$COMMAND --restart=on-failure" COMMAND="$COMMAND --volume=/var/run/dbus/:/var/run/dbus/" @@ -57,10 +57,28 @@ docker_run() { sh build_docker.sh --tag $TAG fi - echo $COMMAND + if [ "$TIME_ZONE" != "" ]; then + COMMAND="$COMMAND --env TZ=$TIME_ZONE" + fi + + if [ "$API" != false ]; then + COMMAND="$COMMAND --env API=$API" + fi + + if [ "$DEBUG" = true ]; then + COMMAND="$COMMAND --env DEBUG=$DEBUG" + COMMAND="$COMMAND $CONTAINER:$TAG" + echo + echo $COMMAND + echo + echo DEBUG MODE + else + COMMAND="$COMMAND $CONTAINER:$TAG" + fi + echo Start container... echo - $COMMAND $CONTAINER + $COMMAND docker container rm $CONTAINER_NAME > /dev/null 2>&1 } @@ -68,7 +86,11 @@ docker_run() { while [ "$1" != "" ]; do case $1 in -d ) - D="-d" + BACKGROUND="-d" + shift + ;; + --debug ) + DEBUG=true shift ;; -a | --api) From 63e52fface6d482882398ae658c05d10c2989c79 Mon Sep 17 00:00:00 2001 From: DasMoorhuhn Date: Wed, 3 Jul 2024 02:06:21 +0200 Subject: [PATCH 10/10] set image to latest --- README.md | 71 +++++++++++++++++-------------- build_docker.sh | 10 ++++- build_docker_multi_platforn.sh | 57 ++++++++++++++++++++++--- python/src/log_data.py | 8 +++- run_docker.sh => run_gateway.sh | 34 ++++++++++++--- stop_docker.sh => stop_gateway.sh | 0 6 files changed, 132 insertions(+), 48 deletions(-) rename run_docker.sh => run_gateway.sh (79%) rename stop_docker.sh => stop_gateway.sh (100%) diff --git a/README.md b/README.md index a5e2f66..b90e968 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,10 @@ * [ATC_MiThermometer_Gateway](#atc_mithermometer_gateway) * [Roadmap](#roadmap) * [Getting started](#getting-started) - * [Run Gateway](#run-gateway) + * [Preconditions](#preconditions) * [Shell Scripts](#shell-scripts) - * [Docker](#docker) + * [Start Gateway](#start-gateway) + * [Build your own docker container](#build-your-own-docker-container) * [MicroPython for MicroController](#micropython-for-microcontroller) * [Resources](#resources) @@ -42,23 +43,12 @@ Python gateway for the [custom firmware](https://github.com/atc1441/ATC_MiThermo First of all, you need to flash the [custom firmware](https://github.com/atc1441/ATC_MiThermometer) on your LYWSD03MMC device. A step-by-step guid is in his youtube channel, the video is linked on his GitHub repo. It's straight forward and does not require any special hardware. -## Run Gateway - -The libraries are needed to be installed as root, because the gateway itself needs to be executed as root, else it is not able to use the bluetooth adapter. In the future, I try to do it better. Also in the future it will be much easier to start and install. +## Preconditions Install `bluez`. It's needed for bluepy to communicate with the bluetooth adapter. ```bash sudo apt-get install -y bluez ``` -Install PIP Libraries -```bash -sudo pip3 install -r python/requirements.txt -``` -Run Gateway -```bash -cd python/src -sudo python3 main.py -``` ## Shell Scripts @@ -66,36 +56,53 @@ sudo python3 main.py | Arg | Meaning | Default | |---------------|----------------------------|---------------------------------------| -| -t \| --tag | Set a tag for build | develop | +| -t \| --tag | Set a tag for build | latest | | -i \| --image | Set a image name for build | dasmoorhuhn/atc-mithermometer-gateway | | -h \| --help | Get this help in the CLI | | **run_docker.sh** -| Arg | Meaning | Default | -|-------------------|----------------------------------------------|---------------| -| -d | Run in Backgrund | | -| -t \| --tag | Set a docker tag | develop | -| -b \| --build | Build the image before running the container | | -| -l \| --loop | Start the gateway in looping mode | | -| -a \| --api | Start with the API | false | -| -tz \| --timezone | Set the timezone | Europe/Berlin | -| -to \| --timeout | Set the timeout for the bluetooth scan | 20 | -| -h \| --help | Get this dialog in CLI | | +| Arg | Meaning | Default | +|---------------------|------------------------------------------------------------------------------------------------------------------------|----------------------| +| -d | Run in Backgrund | false | +| -t \| --tag | Set a docker tag | latest | +| -b \| --build | Build the image before running the container | false | +| -l \| --loop | Start the gateway in looping mode | false (40s fallback) | +| -i \| --interactive | Start the container in interactive mode. That means, you can read the console output in real time and abort via STRG+C | false | +| -a \| --api | Start with the API | false | +| -v \| --volume | Set the volume, where the data from the gateway will be stored. Use relative path like /home/user/gateway/data | data | +| -tz \| --timezone | Set the timezone | Europe/Berlin | +| -to \| --timeout | Set the timeout for the bluetooth scan | 20 | +| -h \| --help | Get this dialog in CLI | | +| --debug | Activate debug mode. Meant for fixing errors or development. | false | -## Docker +## Start Gateway -Build docker container () +For getting started, you need to download the `run_gateway.sh` or build the docker run commands by your own. + +Run Gateway ```bash -sh build_docker.sh -# Or -sh build_docker.sh -i your-image-name -t your-tag +sh run_gateway.sh ``` -Run docker container. Killing the hosts bluetooth service is needed to access it from the docker container. + +Run Gateway with specified volume for persistence data, loop interval of 40 seconds and interactive mode ```bash -sudo sh run_docker.sh +sh run_gateway.sh --volume /home/username/data --loop 40 --interactive ``` +## Build your own docker container + +Build for your current platform (Without parameters, default values will be chosen) +```bash +sh build_docker.sh --image you/your-image-name you/your-image-name +``` + +For building the docker container for multiple platforms at once, you need to have [docker buildx](https://github.com/docker/buildx) installed. (Without parameters, default values will be chosen) +```bash +sh build_docker_multi_platforn.sh --platforms linux/amd64,linux/arm64 --image you/your-image-name you/your-image-name +``` + + ## MicroPython for MicroController Coming when I develop it... diff --git a/build_docker.sh b/build_docker.sh index 4e88e28..94981ed 100644 --- a/build_docker.sh +++ b/build_docker.sh @@ -1,10 +1,16 @@ -TAG=develop +TAG=latest IMAGE=dasmoorhuhn/atc-mithermometer-gateway HELP="USAGE: sh build_docker.sh \n -[ -t | --tag ] Select a tag for building. Default is develop \n +[ -t | --tag ] Select a tag for building. Default is latest \n [ -i | --image ] Select image tag for building. Default is dasmoorhuhn/atc-mithermometer-gateway \n [ -h | --help ] Get this dialog" +docker version > /dev/null 2>&1 +if [ "$?" != 0 ]; then + echo Missing docker. Please install docker and try build again. + exit 1 +fi + docker_build(){ docker build --tag $IMAGE:$TAG . } diff --git a/build_docker_multi_platforn.sh b/build_docker_multi_platforn.sh index 5a36005..b16cbd8 100644 --- a/build_docker_multi_platforn.sh +++ b/build_docker_multi_platforn.sh @@ -1,10 +1,55 @@ -TAG=develop +TAG=latest +IMAGE=dasmoorhuhn/atc-mithermometer-gateway +PLATFORMS=linux/amd64,linux/arm64,linux/arm +HELP="USAGE: sh build_docker.sh \n +[ -t | --tag ] Select a tag for building. Default: latest \n +[ -i | --image ] Select image tag for building. Default: dasmoorhuhn/atc-mithermometer-gateway \n +[ -p | --platforms ] Select the platforms, for which the image should build. Default: linux/amd64,linux/arm64,linux/arm \n +[ -h | --help ] Get this dialog" -set e docker buildx version -unset e +if [ "$?" != 0 ]; then + echo Missing docker buildx. Please install docker buildx from https://github.com/docker/buildx and try build again. + exit 1 +fi -docker buildx create --name builder -docker buildx use builder +create_builder() { + docker buildx create --name builder + docker buildx use builder +} -docker buildx build --tag dasmoorhuhn/atc-mithermometer-gateway:$TAG --platform=linux/amd64,linux/arm64,linux/arm --push . \ No newline at end of file +build_docker() { + create_builder + docker login + docker buildx build --tag $IMAGE:$TAG --platform=$PLATFORMS --push . +} + +while [ "$1" != "" ]; do + case $1 in + -t | --tag ) + shift + TAG=$1 + shift + ;; + -i | --image ) + shift + IMAGE=$1 + shift + ;; + -p | --platforms ) + shift + PLATFORMS=$1 + shift + ;; + -h | --help ) + echo $HELP + exit + ;; + * ) + echo $HELP + echo $1 + exit 1 + esac +done + +build_docker \ No newline at end of file diff --git a/python/src/log_data.py b/python/src/log_data.py index a157481..179a296 100644 --- a/python/src/log_data.py +++ b/python/src/log_data.py @@ -23,15 +23,19 @@ def log_to_json(devices): with open(file_name, 'w') as file: file.write("[]") data = [] - data.append({ + measurements = { "timestamp": data_obj.timestamp, "temperature": data_obj.temperature, "humidity": data_obj.humidity, "battery_percent": data_obj.battery_percent, "battery_volt": data_obj.battery_volt, + "rssi": dev.rssi, "name": from_config.name, "room": from_config.room - }) + } + data.append(measurements) + + print(measurements) if DEBUG else {} with open(file_name, 'w') as file: file.write(json.dumps(data, indent=2)) diff --git a/run_docker.sh b/run_gateway.sh similarity index 79% rename from run_docker.sh rename to run_gateway.sh index 81ef62b..e577324 100644 --- a/run_docker.sh +++ b/run_gateway.sh @@ -1,7 +1,9 @@ -TAG="develop" + + +TAG="latest" CONTAINER="dasmoorhuhn/atc-mithermometer-gateway" CONTAINER_NAME="ATC_MiThermometer_Gateway" -VOLUME=YOUR_VOLUME +VOLUME=data BACKGROUND="" TIME_ZONE="" @@ -14,15 +16,23 @@ TIMEOUT="0" HELP="USAGE: sh run_docker.sh [OPTIONS] \n [ -d ] Run in Backgrund \n -[ -t | --tag ] Set a docker tag \n +[ -t | --tag ] Set a docker tag. Default: latest \n [ -b | --build ] Build the image before running the container \n [ -l | --loop ] Start the gateway in looping mode. e.g.: --loop 40 will set the interval of the loop to 40s. Default is single run mode \n +[ -i | --interactive ] Start the container in interactive mode. That means, you can read the console output in real time and abort via STRG+C \n [ -a | --api ] Start with the API \n +[ -v | --volume ] Set the volume, where the data from the gateway will be stored. Use relative path like /home/user/gateway/data \n [ -tz | --timezone ] Set the timezone. Default is Europe/Berlin \n [ -to | --timeout ] Set the timeout for the bluetooth scan. default is 20s \n [ -h | --help ] Get this dialog \n [ --debug ] Set into debug mode" +docker version > /dev/null 2>&1 +if [ "$?" != 0 ]; then + echo Missing docker. Please install docker and try build again. + exit 1 +fi + docker_run() { sudo killall -9 bluetoothd > /dev/null 2>&1 echo Killing old container... @@ -101,6 +111,11 @@ while [ "$1" != "" ]; do BUILD=true shift ;; + -v | --volume ) + shift + VOLUME=$1 + shift + ;; -tz | --timezone ) shift TIME_ZONE=$1 @@ -118,8 +133,15 @@ while [ "$1" != "" ]; do ;; -l | --loop ) shift - LOOP=$1 - shift + firstchar=`echo $1 | cut -c1-1` + if [ "$firstchar" = "-" ]; then + LOOP=0 + elif [ "$firstchar" = "" ]; then + LOOP=0 + else + LOOP=$1 + shift + fi ;; -i | --interactive ) INTERACTIVE=true @@ -135,5 +157,5 @@ while [ "$1" != "" ]; do esac done -docker_run +# docker_run diff --git a/stop_docker.sh b/stop_gateway.sh similarity index 100% rename from stop_docker.sh rename to stop_gateway.sh