Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bdbfa7d5b4 | |||
| 77aa9cb8f0 | |||
| 97adf45c51 | |||
| dce761eec3 | |||
| 2d7899f74a | |||
| 1a6e6bd1ba | |||
| b461894696 | |||
| 4c6689007d | |||
| 9747010caf | |||
| f74ce76369 | |||
| 7415bb8112 | |||
| d7b3303e50 | |||
| e162948cb8 | |||
| 8663c98410 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -2,5 +2,7 @@ __pycache__
|
||||
devices.yml
|
||||
history.*
|
||||
data/
|
||||
*.json
|
||||
*.iso
|
||||
*.cow
|
||||
.env
|
||||
2578
Charts/data.json
2578
Charts/data.json
File diff suppressed because it is too large
Load Diff
@ -1,204 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Geräte-Diagramme</title>
|
||||
<style>
|
||||
body {
|
||||
background-color: #121212;
|
||||
color: #ffffff;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
.chart-container {
|
||||
width: 80%;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background: #1e1e1e;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.chart {
|
||||
padding: 10px;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
</style>
|
||||
<!-- Lokale Kopie von Chart.js einbinden -->
|
||||
<script src="chart.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Geräte-Diagramme</h1>
|
||||
<div class="datetimepicker-container">
|
||||
<input type="date" id="startDate">
|
||||
<input type="time" id="startTime">
|
||||
<input type="date" id="endDate">
|
||||
<input type="time" id="endTime">
|
||||
<button onclick="applyDateTimeRange()">Anwenden</button>
|
||||
</div>
|
||||
<div id="charts"></div>
|
||||
|
||||
<script>
|
||||
let allData = [];
|
||||
|
||||
fetch('data.json')
|
||||
.then(response => response.json())
|
||||
.then(jsonData => {
|
||||
allData = jsonData;
|
||||
console.log('Loaded data:', allData); // Konsolenausgabe für Debugging
|
||||
displayCharts(jsonData);
|
||||
})
|
||||
.catch(error => console.error('Error loading data:', error));
|
||||
|
||||
function applyDateTimeRange() {
|
||||
const startDate = new Date(document.getElementById('startDate').value);
|
||||
const startTime = document.getElementById('startTime').value;
|
||||
const endDate = new Date(document.getElementById('endDate').value);
|
||||
const endTime = document.getElementById('endTime').value;
|
||||
|
||||
// Kombinieren von Datum und Zeit zu UTC-Zeit
|
||||
const startDateTime = new Date(Date.UTC(startDate.getFullYear(), startDate.getMonth(), startDate.getDate(),
|
||||
parseInt(startTime.substring(0, 2)), parseInt(startTime.substring(3, 5))));
|
||||
const endDateTime = new Date(Date.UTC(endDate.getFullYear(), endDate.getMonth(), endDate.getDate(),
|
||||
parseInt(endTime.substring(0, 2)), parseInt(endTime.substring(3, 5))));
|
||||
|
||||
const filteredData = allData.filter(entry => {
|
||||
const entryDate = new Date(entry.data.timestamp);
|
||||
return entryDate >= startDateTime && entryDate <= endDateTime;
|
||||
});
|
||||
|
||||
console.log('Filtered data:', filteredData); // Konsolenausgabe für Debugging
|
||||
|
||||
displayCharts(filteredData);
|
||||
}
|
||||
|
||||
function displayCharts(data) {
|
||||
document.getElementById('charts').innerHTML = '';
|
||||
const deviceDataMap = new Map();
|
||||
data.forEach(entry => {
|
||||
const deviceMac = entry.device.mac;
|
||||
if (!deviceDataMap.has(deviceMac)) {
|
||||
deviceDataMap.set(deviceMac, {info: entry.device, data: []});
|
||||
}
|
||||
deviceDataMap.get(deviceMac).data.push(entry.data);
|
||||
});
|
||||
|
||||
console.log('Device data map:', deviceDataMap); // Konsolenausgabe für Debugging
|
||||
|
||||
deviceDataMap.forEach((deviceData, deviceMac) => createChart(deviceData));
|
||||
}
|
||||
|
||||
function createChart(deviceData) {
|
||||
const labels = deviceData.data.map(entry => new Date(entry.timestamp).toLocaleTimeString());
|
||||
const temperatures = deviceData.data.map(entry => entry.temperature);
|
||||
const humidities = deviceData.data.map(entry => entry.humidity);
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.className = 'chart-container';
|
||||
|
||||
const deviceInfo = document.createElement('div');
|
||||
deviceInfo.className = 'device-info';
|
||||
deviceInfo.innerHTML = `<strong>${deviceData.info.name}</strong> (${deviceData.info.room})`;
|
||||
container.appendChild(deviceInfo);
|
||||
|
||||
const ctx = document.createElement('canvas');
|
||||
ctx.className = 'chart';
|
||||
container.appendChild(ctx);
|
||||
|
||||
document.getElementById('charts').appendChild(container);
|
||||
|
||||
// Überprüfen, ob Chart.js geladen ist, bevor das Chart-Objekt erstellt wird
|
||||
if (typeof Chart !== 'undefined') {
|
||||
new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Temperatur (°C)',
|
||||
data: temperatures,
|
||||
borderColor: 'rgba(75, 192, 192, 1)',
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
||||
yAxisID: 'y-axis-temp'
|
||||
},
|
||||
{
|
||||
label: 'Feuchtigkeit (%)',
|
||||
data: humidities,
|
||||
borderColor: 'rgba(153, 102, 255, 1)',
|
||||
backgroundColor: 'rgba(153, 102, 255, 0.2)',
|
||||
yAxisID: 'y-axis-humidity'
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
scales: {
|
||||
yAxes: [
|
||||
{
|
||||
id: 'y-axis-temp',
|
||||
type: 'linear',
|
||||
position: 'left',
|
||||
ticks: {
|
||||
beginAtZero: true,
|
||||
fontColor: 'white'
|
||||
},
|
||||
scaleLabel: {
|
||||
display: true,
|
||||
labelString: 'Temperatur (°C)',
|
||||
fontColor: 'white'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'y-axis-humidity',
|
||||
type: 'linear',
|
||||
position: 'right',
|
||||
ticks: {
|
||||
beginAtZero: true,
|
||||
fontColor: 'white'
|
||||
},
|
||||
scaleLabel: {
|
||||
display: true,
|
||||
labelString: 'Feuchtigkeit (%)',
|
||||
fontColor: 'white'
|
||||
}
|
||||
}
|
||||
],
|
||||
xAxes: [{
|
||||
ticks: {
|
||||
fontColor: 'white'
|
||||
}
|
||||
}]
|
||||
},
|
||||
legend: {
|
||||
labels: {
|
||||
fontColor: 'white'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.error('Chart.js is not loaded.');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,181 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Geräte-Diagramme</title>
|
||||
<style>
|
||||
body {
|
||||
background-color: #121212;
|
||||
color: #ffffff;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
.chart-container {
|
||||
width: 80%;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background: #1e1e1e;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.chart {
|
||||
padding: 10px;
|
||||
}
|
||||
.device-info {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.datepicker-container {
|
||||
width: 80%;
|
||||
margin: 20px auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.datepicker-container input, .datepicker-container button {
|
||||
padding: 10px;
|
||||
font-size: 16px;
|
||||
background-color: #1e1e1e;
|
||||
border: 1px solid #444;
|
||||
border-radius: 5px;
|
||||
color: #ffffff;
|
||||
}
|
||||
.datepicker-container button {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Geräte-Diagramme</h1>
|
||||
<div class="datepicker-container">
|
||||
<input type="date" id="startDate">
|
||||
<input type="date" id="endDate">
|
||||
<button onclick="applyDateRange()">Anwenden</button>
|
||||
</div>
|
||||
<div id="charts"></div>
|
||||
|
||||
<script>
|
||||
let allData = [];
|
||||
|
||||
fetch('history.json')
|
||||
.then(response => response.json())
|
||||
.then(jsonData => {
|
||||
allData = jsonData;
|
||||
displayCharts(jsonData);
|
||||
})
|
||||
.catch(error => console.error('Error loading data:', error));
|
||||
|
||||
function applyDateRange() {
|
||||
const startDate = new Date(document.getElementById('startDate').value);
|
||||
const endDate = new Date(document.getElementById('endDate').value);
|
||||
const filteredData = allData.filter(entry => {
|
||||
const entryDate = new Date(entry.data.timestamp);
|
||||
return entryDate >= startDate && entryDate <= endDate;
|
||||
});
|
||||
displayCharts(filteredData);
|
||||
}
|
||||
|
||||
function displayCharts(data) {
|
||||
document.getElementById('charts').innerHTML = '';
|
||||
const deviceDataMap = new Map();
|
||||
data.forEach(entry => {
|
||||
const deviceMac = entry.device.mac;
|
||||
if (!deviceDataMap.has(deviceMac)) {
|
||||
deviceDataMap.set(deviceMac, {info: entry.device, data: []});
|
||||
}
|
||||
deviceDataMap.get(deviceMac).data.push(entry.data);
|
||||
});
|
||||
|
||||
deviceDataMap.forEach((deviceData, deviceMac) => createChart(deviceData));
|
||||
}
|
||||
|
||||
function createChart(deviceData) {
|
||||
const labels = deviceData.data.map(entry => new Date(entry.timestamp).toLocaleTimeString());
|
||||
const temperatures = deviceData.data.map(entry => entry.temperature);
|
||||
const humidities = deviceData.data.map(entry => entry.humidity);
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.className = 'chart-container';
|
||||
|
||||
const deviceInfo = document.createElement('div');
|
||||
deviceInfo.className = 'device-info';
|
||||
deviceInfo.innerHTML = `<strong>${deviceData.info.name}</strong> (${deviceData.info.room})`;
|
||||
container.appendChild(deviceInfo);
|
||||
|
||||
const ctx = document.createElement('canvas');
|
||||
ctx.className = 'chart';
|
||||
container.appendChild(ctx);
|
||||
|
||||
document.getElementById('charts').appendChild(container);
|
||||
|
||||
new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Temperatur (°C)',
|
||||
data: temperatures,
|
||||
borderColor: 'rgba(75, 192, 192, 1)',
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
||||
yAxisID: 'y-axis-temp'
|
||||
},
|
||||
{
|
||||
label: 'Feuchtigkeit (%)',
|
||||
data: humidities,
|
||||
borderColor: 'rgba(153, 102, 255, 1)',
|
||||
backgroundColor: 'rgba(153, 102, 255, 0.2)',
|
||||
yAxisID: 'y-axis-humidity'
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
scales: {
|
||||
yAxes: [
|
||||
{
|
||||
id: 'y-axis-temp',
|
||||
type: 'linear',
|
||||
position: 'left',
|
||||
ticks: {
|
||||
beginAtZero: true,
|
||||
fontColor: 'white'
|
||||
},
|
||||
scaleLabel: {
|
||||
display: true,
|
||||
labelString: 'Temperatur (°C)',
|
||||
fontColor: 'white'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'y-axis-humidity',
|
||||
type: 'linear',
|
||||
position: 'right',
|
||||
ticks: {
|
||||
beginAtZero: true,
|
||||
fontColor: 'white'
|
||||
},
|
||||
scaleLabel: {
|
||||
display: true,
|
||||
labelString: 'Feuchtigkeit (%)',
|
||||
fontColor: 'white'
|
||||
}
|
||||
}
|
||||
],
|
||||
xAxes: [{ // Hier war der Fehler
|
||||
ticks: {
|
||||
fontColor: 'white'
|
||||
}
|
||||
}]
|
||||
},
|
||||
legend: {
|
||||
labels: {
|
||||
fontColor: 'white'
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -38,13 +38,16 @@ FROM python:3.12-alpine3.20
|
||||
WORKDIR /src
|
||||
COPY ./python/src/ .
|
||||
COPY ./python/docker_entrypoint.sh /
|
||||
RUN mkdir data
|
||||
RUN mkdir -p data/log
|
||||
VOLUME /src/data
|
||||
|
||||
RUN apk add --no-cache sudo bluez tzdata
|
||||
ENV TZ=Europe/Berlin
|
||||
ENV DOCKER=true
|
||||
ENV API=false
|
||||
ENV NAME=ATC_MiThermometer_Gateway
|
||||
ENV VERSION=24-08-29
|
||||
ENV MODE=1
|
||||
|
||||
# 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
|
||||
|
||||
@ -85,9 +85,9 @@ Run Gateway
|
||||
sh run_gateway.sh
|
||||
```
|
||||
|
||||
Run Gateway with specified volume for persistence data, loop interval of 40 seconds and interactive mode
|
||||
Run Gateway with specified volume for persistence data, api, loop interval of 40 seconds and interactive mode
|
||||
```bash
|
||||
sh run_gateway.sh --volume /home/username/data --loop 40 --interactive
|
||||
sh run_gateway.sh --volume $PWD/data --loop 40 --interactive --api
|
||||
```
|
||||
|
||||
## Build your own docker container
|
||||
@ -111,3 +111,4 @@ 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/
|
||||
- https://github.com/jholtmann/ip_discovery
|
||||
10
example.env
Normal file
10
example.env
Normal file
@ -0,0 +1,10 @@
|
||||
BACKGROUND=
|
||||
TIME_ZONE=
|
||||
NAME=
|
||||
INTERACTIVE=true
|
||||
BUILD=true
|
||||
API=true
|
||||
DEBUG=INFO
|
||||
MODE=1
|
||||
LOOP=20
|
||||
TIMEOUT=20
|
||||
@ -7,4 +7,5 @@ if [ "$API" = true ]; then
|
||||
sleep 1
|
||||
fi
|
||||
|
||||
python3.12 start_discovery_server.py &
|
||||
python3.12 main.py
|
||||
@ -4,3 +4,5 @@ bs4
|
||||
requests
|
||||
flask
|
||||
flask_cors
|
||||
FindMyIP
|
||||
paho-mqtt
|
||||
@ -4,6 +4,7 @@ from flask import jsonify
|
||||
from flask import send_from_directory
|
||||
from flask_cors import CORS
|
||||
from flask_cors import cross_origin
|
||||
from check_update import check_for_update
|
||||
|
||||
|
||||
class API:
|
||||
@ -17,22 +18,45 @@ class API:
|
||||
self.app.config['CORS_HEADERS'] = 'Content-Type'
|
||||
|
||||
# --------Static Routes-------
|
||||
@self.app.route('/api')
|
||||
@cross_origin()
|
||||
def serve_root():
|
||||
workdir, filename = os.path.split(os.path.abspath(__file__))
|
||||
update = check_for_update()
|
||||
root_dict = {
|
||||
"version": {
|
||||
"version": os.getenv('VERSION'),
|
||||
"update_available": update.update_available if update is not None else None,
|
||||
"up_to_date": update.up_to_date if update is not None else None,
|
||||
"develop_version": update.development if update is not None else None,
|
||||
},
|
||||
"mode": os.getenv('MODE'),
|
||||
"name": os.getenv('NAME'),
|
||||
"info": {
|
||||
"files_size_sum": self.get_file_size(),
|
||||
"files": os.listdir(f'{workdir}/data')
|
||||
}
|
||||
}
|
||||
return jsonify(root_dict)
|
||||
|
||||
@self.app.route('/api/json/<path:path>')
|
||||
@cross_origin()
|
||||
def serve_json(path):
|
||||
return send_from_directory(f'{workdir}/data', path)
|
||||
|
||||
@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)
|
||||
# --------Helpers-------
|
||||
def get_file_size(self):
|
||||
workdir, filename = os.path.split(os.path.abspath(__file__))
|
||||
files = os.listdir(f'{workdir}/data')
|
||||
sizes = 0
|
||||
for file in files:
|
||||
if file.endswith('.json'): sizes += os.path.getsize(f'{workdir}/data/{file}')
|
||||
return sizes
|
||||
|
||||
|
||||
api = API()
|
||||
|
||||
@ -3,6 +3,8 @@ from bluepy.btle import Scanner
|
||||
|
||||
from data_class import Data
|
||||
from devices import get_device
|
||||
from logger import get_logger
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# This is the list, where the responses will be stored from the `handleDiscovery`
|
||||
devices = []
|
||||
@ -38,9 +40,9 @@ 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: ?")
|
||||
print(f'\tTemp: {data_obj.temperature}°C, Humid: {data_obj.humidity}%, Batt: {data_obj.battery_percent}%\n')
|
||||
try:logger.info(f"Device: {dev.addr.upper()} ({dev.addrType}), RSSI: {dev.rssi}dB, Room: {device_from_config.room}")
|
||||
except:logger.info(f"Device: {dev.addr.upper()} ({dev.addrType}), RSSI: {dev.rssi}dB, Room: ?")
|
||||
logger.info(f'\tTemp: {data_obj.temperature}°C, Humid: {data_obj.humidity}%, Batt: {data_obj.battery_percent}%')
|
||||
return True
|
||||
|
||||
|
||||
@ -52,7 +54,7 @@ def cleanup():
|
||||
def start_discovery(timeout=20):
|
||||
cleanup()
|
||||
global devices
|
||||
print(f'Start discovery with timout {timeout}s...')
|
||||
logger.info(f'Start discovery with timout {timeout}s...')
|
||||
|
||||
scanner = Scanner().withDelegate(ScanDelegate())
|
||||
scanner.scan(timeout=timeout, passive=False)
|
||||
74
python/src/check_update.py
Normal file
74
python/src/check_update.py
Normal file
@ -0,0 +1,74 @@
|
||||
import os
|
||||
import json
|
||||
import requests
|
||||
from logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class State:
|
||||
def __init__(self, version_int:int):
|
||||
self.version_int:int = version_int
|
||||
self.version_str:str = f"{str(version_int)[0]}{str(version_int)[1]}-{str(version_int)[2]}{str(version_int)[3]}-{str(version_int)[4]}{str(version_int)[5]}"
|
||||
self.update_available:bool = False
|
||||
self.up_to_date:bool = False
|
||||
self.development:bool = False
|
||||
|
||||
|
||||
class Release:
|
||||
def __init__(self, data:dict):
|
||||
self.name = data['name']
|
||||
self.tag_name = data['tag_name']
|
||||
self.description = data['description']
|
||||
self.created_at = data['created_at']
|
||||
self.released_at = data['released_at']
|
||||
self.upcoming_release = data['upcoming_release']
|
||||
self.version_int = int(self.tag_name.replace("-", ""))
|
||||
|
||||
|
||||
def check_for_update():
|
||||
try: version_current = int(os.getenv('VERSION').replace("-", ""))
|
||||
except:
|
||||
logger.error("Error getting current version")
|
||||
return
|
||||
project_id = 58341398
|
||||
request = f"https://gitlab.com/api/v4/projects/{project_id}/releases"
|
||||
response = requests.get(url=request, timeout=1)
|
||||
if not response.ok: return
|
||||
|
||||
releases_json = json.loads(response.text)
|
||||
# Date, Object
|
||||
latest = [0, None]
|
||||
for release_json in releases_json:
|
||||
release = Release(release_json)
|
||||
if release.version_int > latest[0]:
|
||||
latest[0] = release.version_int
|
||||
latest[1] = release
|
||||
logger.debug(repr(latest))
|
||||
|
||||
release = latest[1]
|
||||
if release.version_int > version_current:
|
||||
state = State(release.version_int)
|
||||
state.update_available = True
|
||||
return state
|
||||
|
||||
elif release.version_int == version_current:
|
||||
state = State(release.version_int)
|
||||
state.up_to_date = True
|
||||
return state
|
||||
|
||||
else:
|
||||
state = State(release.version_int)
|
||||
state.development = True
|
||||
return state
|
||||
|
||||
|
||||
def print_state(state:State):
|
||||
if state is None: return
|
||||
logger.info(f"Current version: {os.getenv('VERSION')}")
|
||||
if state.update_available:
|
||||
logger.info(f"Update available: {state.version_str}")
|
||||
if state.up_to_date:
|
||||
logger.info(f"Up to date")
|
||||
if state.development:
|
||||
logger.info(f"Development Version")
|
||||
77
python/src/find_gateways.py
Normal file
77
python/src/find_gateways.py
Normal file
@ -0,0 +1,77 @@
|
||||
# https://github.com/jholtmann/ip_discovery
|
||||
|
||||
import os
|
||||
import FindMyIP
|
||||
from socket import *
|
||||
from helpers import get_unix_time
|
||||
from logger import get_logger
|
||||
|
||||
DEBUG = True if os.environ.get('DEBUG') is not None else False
|
||||
DISCOVERY_ACK = 'IP_DISCOVERY_ACK'.encode() # ACK for broadcast
|
||||
DISCOVERY_RSP_GTW = 'IP_DISCOVERY_RSP_GTW'.encode() # RSP for gateway
|
||||
DISCOVERY_RSP_MSH = 'IP_DISCOVERY_RSP_MSH'.encode() # RSP for mesh
|
||||
DISCOVERY_TIMEOUT = 0.5
|
||||
SOCKET_TIMEOUT = 0.2
|
||||
PORT_SERVER = 9434
|
||||
PORT_CLIENT = 9435
|
||||
|
||||
|
||||
def start_discovery_server():
|
||||
logger = get_logger(__name__)
|
||||
sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
|
||||
sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
|
||||
sock.setsockopt(SOL_SOCKET, SO_BROADCAST, 1)
|
||||
server_address = ('', PORT_SERVER)
|
||||
local_ip = FindMyIP.internal()
|
||||
try:
|
||||
sock.bind(server_address)
|
||||
logger.info("Started discovery socket")
|
||||
while True:
|
||||
data, addr = sock.recvfrom(4096)
|
||||
logger.debug(f"Received a packet from {addr}")
|
||||
logger.debug(f"{addr[0]} | {local_ip}")
|
||||
logger.debug(f"{data} | {DISCOVERY_ACK}")
|
||||
|
||||
if data == DISCOVERY_ACK:
|
||||
logger.debug("ACK accepted")
|
||||
if str(addr[0]) == str(local_ip): continue
|
||||
logger.debug("IP accepted")
|
||||
sock.sendto(DISCOVERY_RSP_GTW, (addr[0], PORT_CLIENT))
|
||||
logger.debug(f"Send ACK to {addr}")
|
||||
|
||||
except Exception as err:
|
||||
logger.error(err)
|
||||
sock.close()
|
||||
|
||||
|
||||
def start_discovery_client():
|
||||
print("Started discovery client")
|
||||
sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
|
||||
sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
|
||||
sock.setsockopt(SOL_SOCKET, SO_BROADCAST, 1)
|
||||
sock.settimeout(SOCKET_TIMEOUT)
|
||||
server_address = ('255.255.255.255', PORT_SERVER)
|
||||
start_time_stamp = get_unix_time()
|
||||
delta = round(get_unix_time() - start_time_stamp, 2)
|
||||
discovered_devices = []
|
||||
|
||||
try:
|
||||
sock.bind(('', PORT_CLIENT))
|
||||
while delta <= DISCOVERY_TIMEOUT:
|
||||
delta = round(get_unix_time() - start_time_stamp, 2)
|
||||
sock.sendto(DISCOVERY_ACK, server_address)
|
||||
data, addr = sock.recvfrom(4096)
|
||||
if data == DISCOVERY_RSP_GTW or data == DISCOVERY_RSP_MSH:
|
||||
if str(addr[0]) in discovered_devices: continue
|
||||
print('IP: ' + str(addr[0]))
|
||||
discovered_devices.append(str(addr[0]))
|
||||
except Exception as err:
|
||||
print(err)
|
||||
|
||||
finally:
|
||||
sock.close()
|
||||
return discovered_devices
|
||||
|
||||
|
||||
devices = start_discovery_client()
|
||||
print(f"Devices: {devices}")
|
||||
16
python/src/helpers.py
Normal file
16
python/src/helpers.py
Normal file
@ -0,0 +1,16 @@
|
||||
import datetime
|
||||
import time
|
||||
|
||||
|
||||
def get_time_now():
|
||||
date_now = datetime.datetime.now()
|
||||
return str(date_now).split(" ")[0]
|
||||
|
||||
|
||||
def get_unix_time():
|
||||
return time.time()
|
||||
|
||||
|
||||
def get_date():
|
||||
date_now = datetime.datetime.now()
|
||||
return str(date_now).split(" ")
|
||||
@ -3,8 +3,8 @@ import sys
|
||||
import json
|
||||
from data_class import Data
|
||||
from devices import Device
|
||||
|
||||
DEBUG = True if os.getenv('DEBUG') == 'true' else False
|
||||
from logger import get_logger
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def log_to_json(devices):
|
||||
@ -15,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) if DEBUG else {}
|
||||
logger.debug(f"Save to {file_name}")
|
||||
|
||||
try:
|
||||
with open(file_name, 'r') as file: data = json.load(file)
|
||||
@ -30,12 +30,12 @@ def log_to_json(devices):
|
||||
"battery_percent": data_obj.battery_percent,
|
||||
"battery_volt": data_obj.battery_volt,
|
||||
"rssi": dev.rssi,
|
||||
"name": from_config.name,
|
||||
"room": from_config.room
|
||||
"name": from_config.name if from_config is not None else "Unknown",
|
||||
"room": from_config.room if from_config is not None else "Unknown"
|
||||
}
|
||||
data.append(measurements)
|
||||
|
||||
print(measurements) if DEBUG else {}
|
||||
logger.debug(measurements)
|
||||
|
||||
with open(file_name, 'w') as file: file.write(json.dumps(data, indent=2))
|
||||
|
||||
|
||||
21
python/src/logger.py
Normal file
21
python/src/logger.py
Normal file
@ -0,0 +1,21 @@
|
||||
import os
|
||||
import logging
|
||||
|
||||
|
||||
def get_logger(logger_name:str, log_file='gateway.log'):
|
||||
logger_name = logger_name.replace('__', '')
|
||||
debug_level = os.getenv('DEBUG').upper()
|
||||
if debug_level not in ('CRITICAL', 'ERROR', 'WARNING', 'WARN', 'INFO', 'DEBUG', 'NOTSET', 'FATAL'):
|
||||
print(f'Loglevel "{debug_level}" is not supported.')
|
||||
exit(0)
|
||||
logger = logging.getLogger(logger_name)
|
||||
logger.setLevel(logging.getLevelName(debug_level))
|
||||
handler = logging.FileHandler(filename=f'data/{log_file}', encoding='utf-8', mode='a')
|
||||
formatter = logging.Formatter('%(asctime)s|%(levelname)s|%(name)s|:%(message)s')
|
||||
handler.setFormatter(formatter)
|
||||
stream_handler = logging.StreamHandler()
|
||||
stream_handler.setFormatter(formatter)
|
||||
logger.addHandler(stream_handler)
|
||||
logger.addHandler(handler)
|
||||
logger.info(f"Logger {logger_name} init done")
|
||||
return logger
|
||||
@ -1,10 +1,12 @@
|
||||
from time import sleep
|
||||
from log_data import log_to_json
|
||||
from discovery import start_discovery
|
||||
from ble_discovery import start_discovery
|
||||
from logger import get_logger
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def start_loop(interval=40, timeout=20):
|
||||
print(f"Starting loop with interval {interval}s")
|
||||
logger.info(f"Starting loop with interval {interval}s")
|
||||
while True:
|
||||
devices = start_discovery(timeout=timeout)
|
||||
log_to_json(devices)
|
||||
|
||||
@ -1,35 +1,45 @@
|
||||
import os
|
||||
from discovery import start_discovery
|
||||
from logger import get_logger
|
||||
from ble_discovery import start_discovery
|
||||
from log_data import log_to_json
|
||||
from loop import start_loop
|
||||
from check_update import check_for_update
|
||||
from check_update import print_state
|
||||
|
||||
INTERVAL = 40
|
||||
TIMEOUT = 20
|
||||
DOCKER = True if os.getenv('DOCKER') == 'true' else False
|
||||
DEBUG = True if os.getenv('DEBUG') == 'true' else False
|
||||
DOCKER = os.getenv('DOCKER') == 'true'
|
||||
DEBUG = os.getenv('DEBUG')
|
||||
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("")
|
||||
logger = get_logger(__name__)
|
||||
|
||||
if DOCKER:
|
||||
print("Running in docker")
|
||||
logger.debug(f"INTERVAL: {INTERVAL}")
|
||||
logger.debug(f"TIMEOUT: {TIMEOUT}")
|
||||
logger.debug(f"interval: {interval}")
|
||||
logger.debug(f"timeout: {timeout}")
|
||||
logger.debug(f"DOCKER: {DOCKER}")
|
||||
logger.debug(f"DEBUG: {DEBUG}")
|
||||
logger.debug(f"VERSION: {os.getenv('VERSION')}")
|
||||
|
||||
try:INTERVAL = int(interval)
|
||||
except:pass
|
||||
update_state = check_for_update()
|
||||
print_state(update_state)
|
||||
|
||||
try:TIMEOUT = int(timeout)
|
||||
except:pass
|
||||
try:
|
||||
if DOCKER:
|
||||
logger.info('Running in Docker')
|
||||
|
||||
if interval is None: log_to_json(start_discovery(timeout=TIMEOUT))
|
||||
else:start_loop(INTERVAL, TIMEOUT)
|
||||
try:INTERVAL = int(interval)
|
||||
except:pass
|
||||
|
||||
else:
|
||||
start_loop(interval=40)
|
||||
try:TIMEOUT = int(timeout)
|
||||
except:pass
|
||||
|
||||
if interval is None: log_to_json(start_discovery(timeout=TIMEOUT))
|
||||
else:start_loop(INTERVAL, TIMEOUT)
|
||||
|
||||
else:
|
||||
start_loop(interval=40)
|
||||
except Exception as err:
|
||||
logger.error(err)
|
||||
|
||||
@ -1 +1,15 @@
|
||||
# TODO
|
||||
import paho.mqtt.client as MQTT
|
||||
|
||||
topic_root = "/atc_mithermometer_gateway"
|
||||
|
||||
mqtt = MQTT.Client(mqtt.CallbackAPIVersion.VERSION2)
|
||||
mqtt.connect("192.168.178.140", 1883, 60)
|
||||
|
||||
|
||||
def publish_measurement(mac):
|
||||
topic = f"{topic_root}/measurements/{mac}"
|
||||
topic_temp = f"{topic}/temperature"
|
||||
topic_humid = f"{topic}/humidity"
|
||||
topic_battery = f"{topic}/battery"
|
||||
|
||||
mqtt.publish(topic_temp, )
|
||||
|
||||
2
python/src/start_discovery_server.py
Normal file
2
python/src/start_discovery_server.py
Normal file
@ -0,0 +1,2 @@
|
||||
from find_gateways import start_discovery_server
|
||||
start_discovery_server()
|
||||
@ -1,20 +1,10 @@
|
||||
|
||||
|
||||
TAG="latest"
|
||||
CONTAINER="dasmoorhuhn/atc-mithermometer-gateway"
|
||||
CONTAINER_NAME="ATC_MiThermometer_Gateway"
|
||||
VOLUME=data
|
||||
VOLUME=$(pwd)/data
|
||||
|
||||
BACKGROUND=""
|
||||
TIME_ZONE=""
|
||||
INTERACTIVE=false
|
||||
BUILD=false
|
||||
API=false
|
||||
DEBUG=false
|
||||
LOOP="0"
|
||||
TIMEOUT="0"
|
||||
|
||||
HELP="USAGE: sh run_docker.sh [OPTIONS] \n
|
||||
HELP="Using any command line argument except -d bypasses the .env file\n\n
|
||||
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
|
||||
@ -22,6 +12,8 @@ HELP="USAGE: sh run_docker.sh [OPTIONS] \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
|
||||
[ -n | --name ] Set a custom name for this gateway \n
|
||||
[ -m2 | --mesh-gateway ] Set the mode to mesh gateway. This gateway is meant to expand the bluetooth radius. Default mode is main gateway \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
|
||||
@ -33,12 +25,50 @@ if [ "$?" != 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
check_for_devices_config() {
|
||||
if [ ! -f devices.yml ]; then
|
||||
touch devices.yml
|
||||
echo 'devices:
|
||||
- mac: A4:C1:38:00:00:00
|
||||
name: "my_room"
|
||||
room: "my_room"' >> devices.yml
|
||||
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
|
||||
|
||||
check_for_devices_config
|
||||
|
||||
if [ "$SKIP_ENV" = true ]; then
|
||||
ENV_EXISTS=false
|
||||
|
||||
else
|
||||
if [ -e .env ]
|
||||
then
|
||||
echo Loading .env file
|
||||
export $(cat .env | xargs)
|
||||
ENV_EXISTS=true
|
||||
else
|
||||
echo No env file found
|
||||
ENV_EXISTS=false
|
||||
|
||||
TIME_ZONE=""
|
||||
INTERACTIVE=true
|
||||
BUILD=true
|
||||
API=true
|
||||
DEBUG="INFO"
|
||||
MODE="1"
|
||||
LOOP="40"
|
||||
TIMEOUT="20"
|
||||
fi
|
||||
fi
|
||||
|
||||
COMMAND="docker run $BACKGROUND"
|
||||
COMMAND="$COMMAND --cap-add=SYS_ADMIN"
|
||||
COMMAND="$COMMAND --cap-add=NET_ADMIN"
|
||||
@ -47,6 +77,15 @@ docker_run() {
|
||||
COMMAND="$COMMAND --restart=on-failure"
|
||||
COMMAND="$COMMAND --volume=/var/run/dbus/:/var/run/dbus/"
|
||||
COMMAND="$COMMAND --volume=$VOLUME:/src/data"
|
||||
COMMAND="$COMMAND --volume=$PWD/devices.yml:/src/devices.yml"
|
||||
|
||||
if [ "$ENV_EXISTS" = true ]; then
|
||||
COMMAND="$COMMAND --env-file .env"
|
||||
fi
|
||||
|
||||
if [ "$BACKGROUND" = "--detach" ]; then
|
||||
INTERACTIVE=false
|
||||
fi
|
||||
|
||||
if [ "$INTERACTIVE" = true ]; then
|
||||
COMMAND="$COMMAND --interactive"
|
||||
@ -75,7 +114,15 @@ docker_run() {
|
||||
COMMAND="$COMMAND --env API=$API"
|
||||
fi
|
||||
|
||||
if [ "$DEBUG" = true ]; then
|
||||
if [ "$MODE" != 1 ]; then
|
||||
COMMAND="$COMMAND --env MODE=$MODE"
|
||||
fi
|
||||
|
||||
if [ "$NAME" != "" ]; then
|
||||
COMMAND="$COMMAND --env NAME=$NAME"
|
||||
fi
|
||||
|
||||
if [ "$DEBUG" = "DEBUG" ]; then
|
||||
COMMAND="$COMMAND --env DEBUG=$DEBUG"
|
||||
COMMAND="$COMMAND $CONTAINER:$TAG"
|
||||
echo
|
||||
@ -83,6 +130,7 @@ docker_run() {
|
||||
echo
|
||||
echo DEBUG MODE
|
||||
else
|
||||
COMMAND="$COMMAND --env DEBUG=$DEBUG"
|
||||
COMMAND="$COMMAND $CONTAINER:$TAG"
|
||||
fi
|
||||
|
||||
@ -95,12 +143,18 @@ docker_run() {
|
||||
|
||||
while [ "$1" != "" ]; do
|
||||
case $1 in
|
||||
-se | --skip-env-file )
|
||||
SKIP_ENV=true
|
||||
echo "Skip env file"
|
||||
shift
|
||||
;;
|
||||
-d )
|
||||
BACKGROUND="-d"
|
||||
BACKGROUND="--detach"
|
||||
shift
|
||||
;;
|
||||
--debug )
|
||||
DEBUG=true
|
||||
shift
|
||||
DEBUG=$1
|
||||
shift
|
||||
;;
|
||||
-a | --api)
|
||||
@ -116,6 +170,15 @@ while [ "$1" != "" ]; do
|
||||
VOLUME=$1
|
||||
shift
|
||||
;;
|
||||
-n | --name )
|
||||
shift
|
||||
NAME=$1
|
||||
shift
|
||||
;;
|
||||
-m2 | --mesh-gateway)
|
||||
MODE=2
|
||||
shift
|
||||
;;
|
||||
-tz | --timezone )
|
||||
shift
|
||||
TIME_ZONE=$1
|
||||
@ -157,5 +220,5 @@ while [ "$1" != "" ]; do
|
||||
esac
|
||||
done
|
||||
|
||||
# docker_run
|
||||
docker_run
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user