Merge branch 'develop' into 'main'
Develop See merge request DasMoorhuhn/atc_mithermometer_gateway!2
This commit is contained in:
commit
303410d639
8
.gitignore
vendored
8
.gitignore
vendored
@ -1 +1,7 @@
|
||||
__pycache__
|
||||
__pycache__
|
||||
devices.yml
|
||||
history.*
|
||||
data/
|
||||
*.json
|
||||
*.iso
|
||||
*.cow
|
||||
2
.idea/ATC_Sensor_Gateway.iml
generated
2
.idea/ATC_Sensor_Gateway.iml
generated
@ -2,7 +2,7 @@
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="jdk" jdkName="Python 3.12" jdkType="Python SDK" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
6
.idea/git_toolbox_blame.xml
generated
Normal file
6
.idea/git_toolbox_blame.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GitToolBoxBlameSettings">
|
||||
<option name="version" value="2" />
|
||||
</component>
|
||||
</project>
|
||||
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@ -3,5 +3,5 @@
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.12 (autopicture-v3)" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12" project-jdk-type="Python SDK" />
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10" project-jdk-type="Python SDK" />
|
||||
</project>
|
||||
BIN
.media/demo.gif
Normal file
BIN
.media/demo.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
55
Dockerfile
55
Dockerfile
@ -1,16 +1,53 @@
|
||||
FROM python:3.12
|
||||
# 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
|
||||
|
||||
WORKDIR = /src
|
||||
COPY ./python/requierements.txt /
|
||||
|
||||
COPY python/src/ .
|
||||
COPY python/requierements.txt .
|
||||
COPY python/docker_entrypoint.sh /
|
||||
RUN apk add \
|
||||
make \
|
||||
git \
|
||||
glib-dev \
|
||||
gcc \
|
||||
build-base \
|
||||
freetype-dev \
|
||||
libpng-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 && \
|
||||
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/docker_entrypoint.sh /
|
||||
RUN mkdir data
|
||||
VOLUME /src/data
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y bluez sudo
|
||||
RUN apk add --no-cache sudo bluez tzdata
|
||||
ENV TZ=Europe/Berlin
|
||||
ENV DOCKER=true
|
||||
ENV API=false
|
||||
|
||||
RUN pip3.12 install -r requierements.txt && rm -f requierements.txt
|
||||
# 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
|
||||
|
||||
ENTRYPOINT sh /docker_entrypoint.sh
|
||||
ENTRYPOINT sh /docker_entrypoint.sh
|
||||
|
||||
97
README.md
97
README.md
@ -1,18 +1,31 @@
|
||||
<!-- TOC -->
|
||||
* [ATC_MiThermometer_Gateway](#atc_mithermometer_gateway)
|
||||
* [Roadmap](#roadmap)
|
||||
* [Getting started](#getting-started)
|
||||
* [Preconditions](#preconditions)
|
||||
* [Shell Scripts](#shell-scripts)
|
||||
* [Start Gateway](#start-gateway)
|
||||
* [Build your own docker container](#build-your-own-docker-container)
|
||||
* [MicroPython for MicroController](#micropython-for-microcontroller)
|
||||
* [Resources](#resources)
|
||||
<!-- TOC -->
|
||||
|
||||
# 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/).
|
||||
|
||||

|
||||
|
||||
**Features:**
|
||||
- [DONE] Make in runnable in a docker container (because only cool people are using docker)
|
||||
## 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
|
||||
- [TODO] 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
|
||||
@ -24,41 +37,77 @@ Python gateway for the [custom firmware](https://github.com/atc1441/ATC_MiThermo
|
||||
|
||||
**Current State**
|
||||
|
||||

|
||||

|
||||
|
||||
## Getting started
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
## Shell Scripts
|
||||
|
||||
**build_docker.sh**
|
||||
|
||||
| Arg | Meaning | Default |
|
||||
|---------------|----------------------------|---------------------------------------|
|
||||
| -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 | 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 |
|
||||
|
||||
## Start Gateway
|
||||
|
||||
For getting started, you need to download the `run_gateway.sh` or build the docker run commands by your own.
|
||||
|
||||
Run Gateway
|
||||
```bash
|
||||
cd python/src
|
||||
sudo python3 main.py
|
||||
sh run_gateway.sh
|
||||
```
|
||||
|
||||
### Docker
|
||||
|
||||
Build docker container
|
||||
Run Gateway with specified volume for persistence data, loop interval of 40 seconds and interactive mode
|
||||
```bash
|
||||
docker-compose build
|
||||
```
|
||||
Run docker container. Killing the hosts bluetooth service is needed to access it from the docker container.
|
||||
```bash
|
||||
sudo sh run_docker.sh
|
||||
sh run_gateway.sh --volume /home/username/data --loop 40 --interactive
|
||||
```
|
||||
|
||||
### MicroPython for MicroController
|
||||
## 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...
|
||||
|
||||
|
||||
# Resources
|
||||
- https://pythonspeed.com/articles/alpine-docker-python this article is nuts :D
|
||||
- https://docs.docker.com/build/building/multi-stage/
|
||||
8
bluetooth_tools/search_for_ble.py
Normal file
8
bluetooth_tools/search_for_ble.py
Normal file
@ -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('')
|
||||
40
build_docker.sh
Normal file
40
build_docker.sh
Normal file
@ -0,0 +1,40 @@
|
||||
TAG=latest
|
||||
IMAGE=dasmoorhuhn/atc-mithermometer-gateway
|
||||
HELP="USAGE: sh build_docker.sh \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 .
|
||||
}
|
||||
|
||||
while [ "$1" != "" ]; do
|
||||
case $1 in
|
||||
-t | --tag )
|
||||
shift
|
||||
TAG=$1
|
||||
shift
|
||||
;;
|
||||
-i | --image )
|
||||
shift
|
||||
IMAGE=$1
|
||||
shift
|
||||
;;
|
||||
-h | --help )
|
||||
echo $HELP
|
||||
exit
|
||||
;;
|
||||
* )
|
||||
echo $HELP
|
||||
exit 1
|
||||
esac
|
||||
done
|
||||
|
||||
docker_build
|
||||
55
build_docker_multi_platforn.sh
Normal file
55
build_docker_multi_platforn.sh
Normal file
@ -0,0 +1,55 @@
|
||||
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"
|
||||
|
||||
docker buildx version
|
||||
if [ "$?" != 0 ]; then
|
||||
echo Missing docker buildx. Please install docker buildx from https://github.com/docker/buildx and try build again.
|
||||
exit 1
|
||||
fi
|
||||
|
||||
create_builder() {
|
||||
docker buildx create --name builder
|
||||
docker buildx use builder
|
||||
}
|
||||
|
||||
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
|
||||
@ -2,6 +2,6 @@ version: '3'
|
||||
|
||||
services:
|
||||
atc_mithermometer_gateway:
|
||||
image: atc-mithermometer-gateway:develop
|
||||
container_name: ATC_MiThermometer_Gateway
|
||||
image: dasmoorhuhn/atc-mithermometer-gateway:develop-alpine
|
||||
container_name: ATC_MiThermometer_Gateway_Build
|
||||
build: .
|
||||
@ -1,8 +1,10 @@
|
||||
#!/bin/bash
|
||||
#!/bin/sh
|
||||
|
||||
service dbus start
|
||||
bluetoothd &
|
||||
env > .env
|
||||
|
||||
/bin/bash
|
||||
if [ "$API" = true ]; then
|
||||
python3.12 api_endpoints.py &
|
||||
sleep 1
|
||||
fi
|
||||
|
||||
sudo python3 main.py
|
||||
python3.12 main.py
|
||||
@ -1,2 +1,6 @@
|
||||
bluepy
|
||||
pyyaml
|
||||
pyyaml
|
||||
bs4
|
||||
requests
|
||||
flask
|
||||
flask_cors
|
||||
39
python/src/api_endpoints.py
Normal file
39
python/src/api_endpoints.py
Normal file
@ -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/<path:path>')
|
||||
@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)
|
||||
128
python/src/chart.html
Normal file
128
python/src/chart.html
Normal file
@ -0,0 +1,128 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Messdaten Charts</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns"></script>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
}
|
||||
.chartContainer {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
height: 500px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
canvas {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Messdaten Charts</h1>
|
||||
<div id="charts"></div>
|
||||
|
||||
<script>
|
||||
async function fetchJSONFiles() {
|
||||
try {
|
||||
const response = await fetch('http://localhost:8000/json');
|
||||
const jsonFiles = await response.json();
|
||||
return jsonFiles;
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abrufen der JSON-Dateien:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchData(filename) {
|
||||
try {
|
||||
const response = await fetch(`http://localhost:8000/json/${filename}`);
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(`Fehler beim Abrufen der Datei ${filename}:`, error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function parseChartData(data) {
|
||||
const timestamps = data.map(entry => new Date(entry.timestamp));
|
||||
const temperatures = data.map(entry => entry.temperature);
|
||||
const humidity = data.map(entry => entry.humidity);
|
||||
const name = data.length > 0 ? data[0].name : 'Unbekannt';
|
||||
const room = data.length > 0 ? data[0].room : 'Unbekannt';
|
||||
|
||||
return { timestamps, temperatures, humidity, name, room };
|
||||
}
|
||||
|
||||
async function renderCharts() {
|
||||
const jsonFiles = await fetchJSONFiles();
|
||||
const chartsContainer = document.getElementById('charts');
|
||||
|
||||
for (const file of jsonFiles) {
|
||||
const data = await fetchData(file);
|
||||
const { timestamps, temperatures, humidity, name, room } = parseChartData(data);
|
||||
|
||||
const chartContainer = document.createElement('div');
|
||||
chartContainer.className = 'chartContainer';
|
||||
chartContainer.innerHTML = `<h2>Gerät: ${name}, Raum: ${room}</h2><canvas></canvas>`;
|
||||
chartsContainer.appendChild(chartContainer);
|
||||
|
||||
const ctx = chartContainer.querySelector('canvas').getContext('2d');
|
||||
new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: timestamps,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Temperatur (°C)',
|
||||
data: temperatures,
|
||||
borderColor: 'rgba(255, 99, 132, 1)',
|
||||
backgroundColor: 'rgba(255, 99, 132, 0.2)',
|
||||
fill: false,
|
||||
},
|
||||
{
|
||||
label: 'Luftfeuchtigkeit (%)',
|
||||
data: humidity,
|
||||
borderColor: 'rgba(54, 162, 235, 1)',
|
||||
backgroundColor: 'rgba(54, 162, 235, 0.2)',
|
||||
fill: false,
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
x: {
|
||||
type: 'time',
|
||||
time: {
|
||||
unit: 'minute'
|
||||
}
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: `Gerät: ${name}, Raum: ${room}`
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
renderCharts();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,23 +1,39 @@
|
||||
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: 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)]).upper()
|
||||
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())
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
8
python/src/devices.example.yml
Normal file
8
python/src/devices.example.yml
Normal file
@ -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"
|
||||
@ -1,2 +1,31 @@
|
||||
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 to_json(self):
|
||||
return {
|
||||
"mac": self.mac,
|
||||
"name": self.name,
|
||||
"room": self.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
|
||||
|
||||
|
||||
def get_device(dev):
|
||||
return next((d for d in get_devices() if d.mac == dev.addr.upper()), None)
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
devices:
|
||||
- "A4:C1:38:83:05:E8":
|
||||
name: "My Sensor"
|
||||
room: "My Room"
|
||||
|
||||
- "...":
|
||||
name: "..."
|
||||
room: "..."
|
||||
@ -1,10 +1,8 @@
|
||||
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_device
|
||||
|
||||
# This is the list, where the responses will be stored from the `handleDiscovery`
|
||||
devices = []
|
||||
@ -19,9 +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):
|
||||
devices.append([dev, data_obj])
|
||||
device_from_config = get_device(dev)
|
||||
devices.append([dev, data_obj, device_from_config])
|
||||
|
||||
@staticmethod
|
||||
def is_temperature(sdid, val):
|
||||
@ -35,31 +35,26 @@ class ScanDelegate(DefaultDelegate):
|
||||
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 = 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 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],
|
||||
}
|
||||
|
||||
def cleanup():
|
||||
global devices
|
||||
devices = []
|
||||
|
||||
|
||||
def start_discovery(timeout=10.0):
|
||||
def start_discovery(timeout=20):
|
||||
cleanup()
|
||||
global devices
|
||||
print(f'Start discovery with timout {timeout}s...')
|
||||
|
||||
scanner = Scanner().withDelegate(ScanDelegate())
|
||||
scanner.scan(timeout=timeout, passive=True)
|
||||
scanner.scan(timeout=timeout, passive=False)
|
||||
|
||||
return devices
|
||||
|
||||
49
python/src/log_data.py
Normal file
49
python/src/log_data.py
Normal file
@ -0,0 +1,49 @@
|
||||
import os
|
||||
import sys
|
||||
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__))
|
||||
|
||||
for device in devices:
|
||||
dev, data_obj, from_config = device
|
||||
data_obj: Data
|
||||
from_config: Device
|
||||
file_name = f'{workdir}/data/{str(data_obj.mac).replace(":", "-")}.json'
|
||||
print(file_name) if DEBUG else {}
|
||||
|
||||
try:
|
||||
with open(file_name, 'r') as file: data = json.load(file)
|
||||
except:
|
||||
with open(file_name, 'w') as file: file.write("[]")
|
||||
data = []
|
||||
|
||||
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))
|
||||
|
||||
|
||||
def log_to_mongodb(data):
|
||||
pass
|
||||
|
||||
|
||||
def log_to_mqtt(data):
|
||||
pass
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
from time import sleep
|
||||
|
||||
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:
|
||||
start_discovery()
|
||||
devices = start_discovery(timeout=timeout)
|
||||
log_to_json(devices)
|
||||
sleep(interval)
|
||||
|
||||
@ -1,19 +1,35 @@
|
||||
import os
|
||||
from discovery import start_discovery
|
||||
from data_class import Data
|
||||
from log_data import log_to_json
|
||||
from loop import start_loop
|
||||
|
||||
devices = start_discovery()
|
||||
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 len(devices) > 0:
|
||||
for device_list in devices:
|
||||
data = device_list[1]
|
||||
device = device_list[0]
|
||||
if DOCKER:
|
||||
print("Running in docker")
|
||||
|
||||
data:Data
|
||||
# print(f'Temp: {data.temperature}°C, Humid: {data.humidity}%, Batt: {data.battery_percent}%')
|
||||
try:INTERVAL = int(interval)
|
||||
except:pass
|
||||
|
||||
try:TIMEOUT = int(timeout)
|
||||
except:pass
|
||||
|
||||
if interval is None: log_to_json(start_discovery(timeout=TIMEOUT))
|
||||
else:start_loop(INTERVAL, TIMEOUT)
|
||||
|
||||
else:
|
||||
print('No devices found')
|
||||
|
||||
|
||||
start_loop(interval=40)
|
||||
|
||||
1
python/src/mqtt.py
Normal file
1
python/src/mqtt.py
Normal file
@ -0,0 +1 @@
|
||||
# TODO
|
||||
@ -1,9 +0,0 @@
|
||||
sudo killall -9 bluetoothd
|
||||
docker stop atc-mithermometer-gateway:develop
|
||||
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
|
||||
159
run_gateway.sh
Normal file
159
run_gateway.sh
Normal file
@ -0,0 +1,159 @@
|
||||
TAG="latest"
|
||||
CONTAINER="dasmoorhuhn/atc-mithermometer-gateway"
|
||||
CONTAINER_NAME="ATC_MiThermometer_Gateway"
|
||||
VOLUME=data
|
||||
|
||||
BACKGROUND=""
|
||||
TIME_ZONE=""
|
||||
INTERACTIVE=false
|
||||
BUILD=false
|
||||
API=false
|
||||
DEBUG=false
|
||||
LOOP="0"
|
||||
TIMEOUT="0"
|
||||
|
||||
HELP="USAGE: sh run_docker.sh [OPTIONS] \n
|
||||
[ -d ] Run in Backgrund \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...
|
||||
docker stop $CONTAINER_NAME > /dev/null 2>&1
|
||||
docker container rm $CONTAINER_NAME > /dev/null 2>&1
|
||||
|
||||
COMMAND="docker run $BACKGROUND"
|
||||
COMMAND="$COMMAND --cap-add=SYS_ADMIN"
|
||||
COMMAND="$COMMAND --cap-add=NET_ADMIN"
|
||||
COMMAND="$COMMAND --net=host"
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
docker container rm $CONTAINER_NAME > /dev/null 2>&1
|
||||
}
|
||||
|
||||
while [ "$1" != "" ]; do
|
||||
case $1 in
|
||||
-d )
|
||||
BACKGROUND="-d"
|
||||
shift
|
||||
;;
|
||||
--debug )
|
||||
DEBUG=true
|
||||
shift
|
||||
;;
|
||||
-a | --api)
|
||||
API=true
|
||||
shift
|
||||
;;
|
||||
-b | --build )
|
||||
BUILD=true
|
||||
shift
|
||||
;;
|
||||
-v | --volume )
|
||||
shift
|
||||
VOLUME=$1
|
||||
shift
|
||||
;;
|
||||
-tz | --timezone )
|
||||
shift
|
||||
TIME_ZONE=$1
|
||||
shift
|
||||
;;
|
||||
-to | --timeout )
|
||||
shift
|
||||
TIMEOUT=$1
|
||||
shift
|
||||
;;
|
||||
-t | --tag )
|
||||
shift
|
||||
TAG=$1
|
||||
shift
|
||||
;;
|
||||
-l | --loop )
|
||||
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
|
||||
shift
|
||||
;;
|
||||
-h | --help )
|
||||
echo $HELP
|
||||
exit
|
||||
;;
|
||||
* )
|
||||
echo $HELP
|
||||
exit 1
|
||||
esac
|
||||
done
|
||||
|
||||
docker_run
|
||||
|
||||
3
stop_gateway.sh
Normal file
3
stop_gateway.sh
Normal file
@ -0,0 +1,3 @@
|
||||
echo Stopping container gracefully...
|
||||
docker stop ATC_MiThermometer_Gateway
|
||||
echo Done
|
||||
Loading…
x
Reference in New Issue
Block a user