Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ef9149ea2 | |||
| 0694941c40 | |||
| 079f901544 | |||
| a3ece51e90 | |||
| e78d7f863a | |||
| 84cae7fc26 | |||
| 6d3755e465 | |||
| ed44da8427 | |||
| 932168125c | |||
| 249b111e67 | |||
| 387c41db05 | |||
| 2eaa1fb847 | |||
| 97adf45c51 | |||
| dce761eec3 | |||
| 2d7899f74a | |||
| 1a6e6bd1ba | |||
| b461894696 | |||
| 4c6689007d | |||
| 9747010caf | |||
| f74ce76369 | |||
| 7415bb8112 | |||
| d7b3303e50 | |||
| e162948cb8 | |||
| 8663c98410 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,3 +5,4 @@ data/
|
|||||||
*.json
|
*.json
|
||||||
*.iso
|
*.iso
|
||||||
*.cow
|
*.cow
|
||||||
|
.env
|
||||||
BIN
.media/ha_integration.png
Normal file
BIN
.media/ha_integration.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
BIN
.media/home_assistant_android_widget.jpg
Normal file
BIN
.media/home_assistant_android_widget.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.3 KiB |
BIN
.media/home_assistant_device.jpg
Normal file
BIN
.media/home_assistant_device.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -3,6 +3,8 @@ FROM python:3.12-alpine3.20 AS pip_build_stage
|
|||||||
|
|
||||||
COPY ./python/requierements.txt /
|
COPY ./python/requierements.txt /
|
||||||
|
|
||||||
|
RUN pip3.12 install setuptools
|
||||||
|
|
||||||
RUN apk add \
|
RUN apk add \
|
||||||
make \
|
make \
|
||||||
git \
|
git \
|
||||||
@@ -38,13 +40,16 @@ FROM python:3.12-alpine3.20
|
|||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY ./python/src/ .
|
COPY ./python/src/ .
|
||||||
COPY ./python/docker_entrypoint.sh /
|
COPY ./python/docker_entrypoint.sh /
|
||||||
RUN mkdir data
|
RUN mkdir -p data/log
|
||||||
VOLUME /src/data
|
VOLUME /src/data
|
||||||
|
|
||||||
RUN apk add --no-cache sudo bluez tzdata
|
RUN apk add --no-cache sudo bluez tzdata
|
||||||
ENV TZ=Europe/Berlin
|
ENV TZ=Europe/Berlin
|
||||||
ENV DOCKER=true
|
ENV DOCKER=true
|
||||||
ENV API=false
|
ENV API=false
|
||||||
|
ENV NAME=ATC_MiThermometer_Gateway
|
||||||
|
ENV VERSION=24-08-29
|
||||||
|
ENV MODE=1
|
||||||
|
|
||||||
# Copy pips from the pip build stage
|
# 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/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
|
||||||
|
|||||||
14
README.md
14
README.md
@@ -22,22 +22,23 @@ Python gateway for the [custom firmware](https://github.com/atc1441/ATC_MiThermo
|
|||||||
- Make in runnable in a docker container (because only cool people are using docker)
|
- 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)
|
- 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
|
- Implement a loop for fetching the data every X seconds
|
||||||
|
- Storing temperature, humidity and battery state as json in a text file
|
||||||
|
- Can run on Raspberry Pi (3, 4, zero w) or any other Linux driven hardware which has BLE and WiFi support
|
||||||
|
|
||||||
**TODOs:**
|
**TODOs:**
|
||||||
- [WIP] Can run on Raspberry Pi (3, 4, zero w) or any other Linux driven hardware which has BLE and WiFi support
|
- [WIP] MQTT publishing with discovery for homeassistant
|
||||||
- [WIP] Storing temperature, humidity and battery state as json in a text file
|
|
||||||
- [TODO] Make a microPython version for using the raspberry pico w or any other microcontroller with BLE and WiFi support
|
- [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] Collect data from multiple devices/gateways
|
||||||
- [TODO] Command line tool for managing the devices
|
- [TODO] Command line tool for managing the devices
|
||||||
- [TODO] Analyzing tool for making statistics
|
- [TODO] Analyzing tool for making statistics
|
||||||
- [TODO] HomeAssistant integration
|
|
||||||
- [TODO] MQTT publishing
|
|
||||||
- [TODO] Maybe... a webinterface. But I suck at web stuff, so I don't know.
|
- [TODO] Maybe... a webinterface. But I suck at web stuff, so I don't know.
|
||||||
- [TODO] Implement other BLE Sensors
|
- [TODO] Implement other BLE Sensors
|
||||||
|
|
||||||
**Current State**
|
**Current State**
|
||||||
|
|
||||||

|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
@@ -85,9 +86,9 @@ Run Gateway
|
|||||||
sh run_gateway.sh
|
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
|
```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
|
## Build your own docker container
|
||||||
@@ -111,3 +112,4 @@ Coming when I develop it...
|
|||||||
# Resources
|
# Resources
|
||||||
- https://pythonspeed.com/articles/alpine-docker-python this article is nuts :D
|
- https://pythonspeed.com/articles/alpine-docker-python this article is nuts :D
|
||||||
- https://docs.docker.com/build/building/multi-stage/
|
- https://docs.docker.com/build/building/multi-stage/
|
||||||
|
- https://github.com/jholtmann/ip_discovery
|
||||||
@@ -5,6 +5,7 @@ HELP="USAGE: sh build_docker.sh \n
|
|||||||
[ -t | --tag ] Select a tag for building. Default: latest \n
|
[ -t | --tag ] Select a tag for building. Default: latest \n
|
||||||
[ -i | --image ] Select image tag for building. Default: dasmoorhuhn/atc-mithermometer-gateway \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
|
[ -p | --platforms ] Select the platforms, for which the image should build. Default: linux/amd64,linux/arm64,linux/arm \n
|
||||||
|
[ -r | --release ] Build a release. Provide the Tag. \n
|
||||||
[ -h | --help ] Get this dialog"
|
[ -h | --help ] Get this dialog"
|
||||||
|
|
||||||
docker buildx version
|
docker buildx version
|
||||||
@@ -24,6 +25,29 @@ build_docker() {
|
|||||||
docker buildx build --tag $IMAGE:$TAG --platform=$PLATFORMS --push .
|
docker buildx build --tag $IMAGE:$TAG --platform=$PLATFORMS --push .
|
||||||
}
|
}
|
||||||
|
|
||||||
|
build_release() {
|
||||||
|
branch=$(git symbolic-ref --short HEAD)
|
||||||
|
git stash
|
||||||
|
git fetch --prune --prune-tags -f
|
||||||
|
git checkout $TAG
|
||||||
|
echo -------------------------------------
|
||||||
|
git branch
|
||||||
|
echo -------------------------------------
|
||||||
|
git status
|
||||||
|
echo -------------------------------------
|
||||||
|
echo "!!PLEASE CHECK IF THIS IS RIGHT!!"
|
||||||
|
sleep 15
|
||||||
|
clear
|
||||||
|
echo Build Tag $TAG
|
||||||
|
build_docker
|
||||||
|
TAG=latest
|
||||||
|
clear
|
||||||
|
echo Build Tag $TAG
|
||||||
|
build_docker
|
||||||
|
git checkout $branch
|
||||||
|
git stash pop
|
||||||
|
}
|
||||||
|
|
||||||
while [ "$1" != "" ]; do
|
while [ "$1" != "" ]; do
|
||||||
case $1 in
|
case $1 in
|
||||||
-t | --tag )
|
-t | --tag )
|
||||||
@@ -31,6 +55,12 @@ while [ "$1" != "" ]; do
|
|||||||
TAG=$1
|
TAG=$1
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
|
-r | --release )
|
||||||
|
shift
|
||||||
|
TAG=$1
|
||||||
|
RELEASE=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
-i | --image )
|
-i | --image )
|
||||||
shift
|
shift
|
||||||
IMAGE=$1
|
IMAGE=$1
|
||||||
@@ -52,4 +82,8 @@ while [ "$1" != "" ]; do
|
|||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
|
if [ "$RELEASE" = true ]; then
|
||||||
|
build_release
|
||||||
|
else
|
||||||
build_docker
|
build_docker
|
||||||
|
fi
|
||||||
12
example.env
Normal file
12
example.env
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
BACKGROUND=
|
||||||
|
TIME_ZONE=
|
||||||
|
NAME=
|
||||||
|
INTERACTIVE=true
|
||||||
|
BUILD=true
|
||||||
|
API=true
|
||||||
|
DEBUG=DEBUG
|
||||||
|
MODE=1
|
||||||
|
LOOP=20
|
||||||
|
TIMEOUT=20
|
||||||
|
MQTT_IP=
|
||||||
|
MQTT_PORT=1883
|
||||||
8
home_assistant_integration/__init__.py
Normal file
8
home_assistant_integration/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
DOMAIN = "atc_mi_thermometer_gateway"
|
||||||
|
|
||||||
|
|
||||||
|
def setup(hass, config):
|
||||||
|
hass.states.set("hello_state.world", "Paulus")
|
||||||
|
|
||||||
|
# Return boolean to indicate that initialization was successful.
|
||||||
|
return True
|
||||||
68
home_assistant_integration/api.py
Normal file
68
home_assistant_integration/api.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
from requests import api
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class EntityState:
|
||||||
|
timestamp: str
|
||||||
|
name:str
|
||||||
|
room:str
|
||||||
|
temperature:float
|
||||||
|
humidity:int
|
||||||
|
battery_percent:int
|
||||||
|
battery_volt:float
|
||||||
|
rssi:int
|
||||||
|
mac:str
|
||||||
|
|
||||||
|
|
||||||
|
def test_api(gateway):
|
||||||
|
request = f'http://{gateway}:8000/api'
|
||||||
|
response = api.get(request)
|
||||||
|
if not response.ok: return False
|
||||||
|
response_json = json.loads(response.text)
|
||||||
|
version = response_json['version']['version']
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def get_state(gateway, entity_mac) -> EntityState | None:
|
||||||
|
request = f'http://{gateway}:8000/api/state/{entity_mac}'
|
||||||
|
response = api.get(request)
|
||||||
|
if not response.ok: return None
|
||||||
|
response_json = json.loads(response.text)
|
||||||
|
return EntityState(response_json['timestamp'],
|
||||||
|
response_json['name'],
|
||||||
|
response_json['room'],
|
||||||
|
response_json['temperature'],
|
||||||
|
response_json['humidity'],
|
||||||
|
response_json['battery_percent'],
|
||||||
|
response_json['battery_volt'],
|
||||||
|
response_json['rssi'],
|
||||||
|
entity_mac)
|
||||||
|
|
||||||
|
|
||||||
|
def get_deices(gateway) -> list | None:
|
||||||
|
request = f'http://{gateway}:8000/api'
|
||||||
|
response = api.get(request)
|
||||||
|
if not response.ok: return None
|
||||||
|
response_json = json.loads(response.text)
|
||||||
|
return response_json['info']['devices']
|
||||||
|
|
||||||
|
|
||||||
|
def get_device(gateway, entity_mac) -> str | None:
|
||||||
|
devices = get_deices(gateway)
|
||||||
|
if entity_mac in devices:
|
||||||
|
index = devices.index(entity_mac)
|
||||||
|
return devices[index]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_entity_state(entity_mac, gateway):
|
||||||
|
device = get_device(gateway, entity_mac)
|
||||||
|
if device is None: return None
|
||||||
|
entity_state = get_state(gateway, device)
|
||||||
|
return entity_state
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
49
home_assistant_integration/discover_gateways.py
Normal file
49
home_assistant_integration/discover_gateways.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
from time import time
|
||||||
|
from socket import socket
|
||||||
|
from socket import AF_INET
|
||||||
|
from socket import SOCK_DGRAM
|
||||||
|
from socket import IPPROTO_UDP
|
||||||
|
from socket import SOL_SOCKET
|
||||||
|
from socket import SO_REUSEADDR
|
||||||
|
from socket import SO_BROADCAST
|
||||||
|
|
||||||
|
|
||||||
|
DISCOVERY_ACK = 'IP_DISCOVERY_ACK'.encode() # ACK for broadcast
|
||||||
|
DISCOVERY_OK = 'IP_DISCOVERY_OK'.encode() # Not used yet
|
||||||
|
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_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 = time()
|
||||||
|
delta = round(time() - start_time_stamp, 2)
|
||||||
|
discovered_devices = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
sock.bind(('', PORT_CLIENT))
|
||||||
|
|
||||||
|
while delta <= DISCOVERY_TIMEOUT:
|
||||||
|
delta = round(time() - start_time_stamp, 2)
|
||||||
|
sock.sendto(DISCOVERY_ACK, server_address)
|
||||||
|
data, addr = sock.recvfrom(4096)
|
||||||
|
|
||||||
|
if str(addr[0]) in discovered_devices: continue
|
||||||
|
if data == DISCOVERY_RSP_GTW:
|
||||||
|
discovered_devices.append(str(addr[0]))
|
||||||
|
|
||||||
|
except Exception as err:
|
||||||
|
print(err)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
sock.close()
|
||||||
|
return discovered_devices[0]
|
||||||
|
|
||||||
12
home_assistant_integration/gateway.py
Normal file
12
home_assistant_integration/gateway.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from api import *
|
||||||
|
from sensor import MiThermometer
|
||||||
|
|
||||||
|
|
||||||
|
def find_all_devices(gateway) -> list:
|
||||||
|
found_devices = []
|
||||||
|
devices = get_deices(gateway=gateway)
|
||||||
|
for device in devices:
|
||||||
|
device_state = get_state(entity_mac=device, gateway=gateway)
|
||||||
|
found_devices.append(MiThermometer(state=device_state))
|
||||||
|
|
||||||
|
return found_devices
|
||||||
82
home_assistant_integration/sensor.py
Normal file
82
home_assistant_integration/sensor.py
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
"""Platform for light integration."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from api import *
|
||||||
|
from .gateway import find_all_devices
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.components.sensor import PLATFORM_SCHEMA
|
||||||
|
from homeassistant.components.sensor import SensorEntity
|
||||||
|
from homeassistant.const import CONF_NAME, CONF_HOST
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
from homeassistant.helpers.typing import DiscoveryInfoType
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger("atc_mi_thermometer_gateway")
|
||||||
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
|
vol.Optional(CONF_NAME): cv.string,
|
||||||
|
vol.Required(CONF_HOST): cv.string,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def setup_platform(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config: ConfigType,
|
||||||
|
add_entities: AddEntitiesCallback,
|
||||||
|
discovery_info: DiscoveryInfoType | None = None):
|
||||||
|
|
||||||
|
devices = find_all_devices(gateway=config[CONF_HOST])
|
||||||
|
for device in devices:
|
||||||
|
add_entities(device)
|
||||||
|
|
||||||
|
|
||||||
|
class MiThermometer(SensorEntity):
|
||||||
|
def __init__(self, state:EntityState):
|
||||||
|
self._mac = state.mac
|
||||||
|
self._online = False
|
||||||
|
self._last_update = ""
|
||||||
|
self._temperature = state.temperature
|
||||||
|
self._humidity = state.temperature
|
||||||
|
self._battery_percentage = state.battery_percent
|
||||||
|
self._rssi = state.rssi
|
||||||
|
self._name = state.name
|
||||||
|
self._room = state.room
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mac(self):
|
||||||
|
return self._mac
|
||||||
|
|
||||||
|
@property
|
||||||
|
def online(self):
|
||||||
|
return self._online
|
||||||
|
|
||||||
|
@property
|
||||||
|
def get_name(self):
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def get_room(self):
|
||||||
|
return self._room
|
||||||
|
|
||||||
|
@property
|
||||||
|
def get_temperature(self):
|
||||||
|
return self._temperature
|
||||||
|
|
||||||
|
@property
|
||||||
|
def get_humidity(self):
|
||||||
|
return self._humidity
|
||||||
|
|
||||||
|
@property
|
||||||
|
def get_battery_percentage(self):
|
||||||
|
return self._battery_percentage
|
||||||
|
|
||||||
|
@property
|
||||||
|
def get_rssi(self):
|
||||||
|
return self._rssi
|
||||||
|
|
||||||
|
def update(self) -> None:
|
||||||
|
state = get_state(gateway=config[CONF_HOST], entity_mac=self._mac)
|
||||||
@@ -7,4 +7,5 @@ if [ "$API" = true ]; then
|
|||||||
sleep 1
|
sleep 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
python3.12 start_discovery_server.py &
|
||||||
python3.12 main.py
|
python3.12 main.py
|
||||||
@@ -4,3 +4,5 @@ bs4
|
|||||||
requests
|
requests
|
||||||
flask
|
flask
|
||||||
flask_cors
|
flask_cors
|
||||||
|
FindMyIP
|
||||||
|
paho-mqtt
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
|
import json
|
||||||
import os
|
import os
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from flask import jsonify
|
from flask import jsonify
|
||||||
from flask import send_from_directory
|
from flask import send_from_directory
|
||||||
from flask_cors import CORS
|
from flask_cors import CORS
|
||||||
from flask_cors import cross_origin
|
from flask_cors import cross_origin
|
||||||
|
from check_update import check_for_update
|
||||||
|
|
||||||
|
from data_class import LogEntry
|
||||||
|
|
||||||
|
|
||||||
class API:
|
class API:
|
||||||
@@ -17,22 +21,65 @@ class API:
|
|||||||
self.app.config['CORS_HEADERS'] = 'Content-Type'
|
self.app.config['CORS_HEADERS'] = 'Content-Type'
|
||||||
|
|
||||||
# --------Static Routes-------
|
# --------Static Routes-------
|
||||||
|
@self.app.route('/api')
|
||||||
|
@cross_origin()
|
||||||
|
def serve_root():
|
||||||
|
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(),
|
||||||
|
"devices": self.get_files()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return jsonify(root_dict)
|
||||||
|
|
||||||
|
@self.app.route('/api/<path:path>')
|
||||||
|
@cross_origin()
|
||||||
|
def serve_json(path):
|
||||||
|
workdir, filename = os.path.split(os.path.abspath(__file__))
|
||||||
|
path += '.json'
|
||||||
|
return send_from_directory(f'{workdir}/data', path)
|
||||||
|
|
||||||
|
@self.app.route('/api/state/<path:path>')
|
||||||
|
@cross_origin()
|
||||||
|
def serve_entity_state(path):
|
||||||
|
workdir, filename = os.path.split(os.path.abspath(__file__))
|
||||||
|
path += '.json'
|
||||||
|
state = json.load(open(f'{workdir}/data/{path}', mode='r'))
|
||||||
|
entity_state = LogEntry(data=state)
|
||||||
|
return jsonify(entity_state.to_json())
|
||||||
|
|
||||||
@self.app.route('/charts')
|
@self.app.route('/charts')
|
||||||
@cross_origin()
|
@cross_origin()
|
||||||
def serve_index():
|
def serve_index():
|
||||||
return send_from_directory('/src', 'chart.html')
|
return send_from_directory('/src', 'chart.html')
|
||||||
|
|
||||||
@self.app.route('/json')
|
# --------Helpers-------
|
||||||
@cross_origin()
|
def get_file_size(self):
|
||||||
def serve_get_list_of_json():
|
|
||||||
workdir, filename = os.path.split(os.path.abspath(__file__))
|
workdir, filename = os.path.split(os.path.abspath(__file__))
|
||||||
return jsonify(os.listdir(f'{workdir}/data'))
|
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
|
||||||
|
|
||||||
@self.app.route('/json/<path:path>')
|
def get_files(self):
|
||||||
@cross_origin()
|
|
||||||
def serve_json(path):
|
|
||||||
workdir, filename = os.path.split(os.path.abspath(__file__))
|
workdir, filename = os.path.split(os.path.abspath(__file__))
|
||||||
return send_from_directory(f'{workdir}/data', path)
|
files = os.listdir(f'{workdir}/data')
|
||||||
|
files_list = []
|
||||||
|
for file in files:
|
||||||
|
if file.endswith('.json'):
|
||||||
|
files_list.append(file.replace('.json', ''))
|
||||||
|
return files_list
|
||||||
|
|
||||||
|
|
||||||
api = API()
|
api = API()
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ from bluepy.btle import Scanner
|
|||||||
|
|
||||||
from data_class import Data
|
from data_class import Data
|
||||||
from devices import get_device
|
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`
|
# This is the list, where the responses will be stored from the `handleDiscovery`
|
||||||
devices = []
|
devices = []
|
||||||
@@ -16,7 +18,7 @@ class ScanDelegate(DefaultDelegate):
|
|||||||
global devices
|
global devices
|
||||||
|
|
||||||
for (sdid, desc, val) in dev.getScanData():
|
for (sdid, desc, val) in dev.getScanData():
|
||||||
if self.is_temperature(sdid, val):
|
if not self.is_temperature(sdid, val): continue
|
||||||
data_obj = Data(val)
|
data_obj = Data(val)
|
||||||
|
|
||||||
if self.is_atc_device(dev, data_obj):
|
if self.is_atc_device(dev, data_obj):
|
||||||
@@ -25,9 +27,7 @@ class ScanDelegate(DefaultDelegate):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_temperature(sdid, val):
|
def is_temperature(sdid, val):
|
||||||
if sdid != 22: return False
|
return sdid == 22 and len(val) == 30
|
||||||
if len(val) != 30: return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_atc_device(dev, data_obj):
|
def is_atc_device(dev, data_obj):
|
||||||
@@ -38,9 +38,9 @@ class ScanDelegate(DefaultDelegate):
|
|||||||
|
|
||||||
device_from_config = 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}")
|
room = device_from_config.room if device_from_config.room is not None else '?'
|
||||||
except:print(f"Device: {dev.addr.upper()} ({dev.addrType}), RSSI: {dev.rssi}dB, Room: ?")
|
logger.info(f"Device: {dev.addr.upper()} ({dev.addrType}), RSSI: {dev.rssi}dB, Room: {room}")
|
||||||
print(f'\tTemp: {data_obj.temperature}°C, Humid: {data_obj.humidity}%, Batt: {data_obj.battery_percent}%\n')
|
logger.info(f'\tTemp: {data_obj.temperature}°C, Humid: {data_obj.humidity}%, Batt: {data_obj.battery_percent}%')
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ def cleanup():
|
|||||||
def start_discovery(timeout=20):
|
def start_discovery(timeout=20):
|
||||||
cleanup()
|
cleanup()
|
||||||
global devices
|
global devices
|
||||||
print(f'Start discovery with timout {timeout}s...')
|
logger.info(f'Start discovery with timout {timeout}s...')
|
||||||
|
|
||||||
scanner = Scanner().withDelegate(ScanDelegate())
|
scanner = Scanner().withDelegate(ScanDelegate())
|
||||||
scanner.scan(timeout=timeout, passive=False)
|
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")
|
||||||
8
python/src/config.py
Normal file
8
python/src/config.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import sys
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Config:
|
||||||
|
API_PORT:int
|
||||||
|
|
||||||
@@ -37,3 +37,28 @@ class Data:
|
|||||||
"battery_volt": self.battery_volt,
|
"battery_volt": self.battery_volt,
|
||||||
"count": self.count,
|
"count": self.count,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class LogEntry:
|
||||||
|
def __init__(self, data):
|
||||||
|
self.name = data['name']
|
||||||
|
self.room = data['room']
|
||||||
|
self.timestamp = data['measurements'][-1]['timestamp']
|
||||||
|
self.temperature = data['measurements'][-1]['temperature']
|
||||||
|
self.humidity = data['measurements'][-1]['humidity']
|
||||||
|
self.battery_percent = data['measurements'][-1]['battery_percent']
|
||||||
|
self.battery_volt = data['measurements'][-1]['battery_volt']
|
||||||
|
self.rssi = data['measurements'][-1]['rssi']
|
||||||
|
|
||||||
|
def to_json(self):
|
||||||
|
return {
|
||||||
|
"name": self.name,
|
||||||
|
"room": self.room,
|
||||||
|
"timestamp": self.timestamp,
|
||||||
|
"temperature": self.temperature,
|
||||||
|
"humidity": self.humidity,
|
||||||
|
"battery_percent": self.battery_percent,
|
||||||
|
"battery_volt": self.battery_volt,
|
||||||
|
"rssi": self.rssi
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
88
python/src/gateway_discovery.py
Normal file
88
python/src/gateway_discovery.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# https://github.com/jholtmann/ip_discovery
|
||||||
|
"""Find other gateways and serve the udp socket"""
|
||||||
|
|
||||||
|
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_OK = 'IP_DISCOVERY_OK'.encode() # Not used yet
|
||||||
|
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 create_udp_socket():
|
||||||
|
sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
|
||||||
|
sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
|
||||||
|
sock.setsockopt(SOL_SOCKET, SO_BROADCAST, 1)
|
||||||
|
return sock
|
||||||
|
|
||||||
|
|
||||||
|
def start_discovery_server():
|
||||||
|
"""Serves the UDP socket for UDP broadcast discovery"""
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
sock = create_udp_socket()
|
||||||
|
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(data)
|
||||||
|
|
||||||
|
if str(addr[0]) == str(local_ip): continue
|
||||||
|
logger.debug("IP accepted")
|
||||||
|
|
||||||
|
if data == DISCOVERY_ACK:
|
||||||
|
logger.debug("ACK accepted")
|
||||||
|
sock.sendto(DISCOVERY_RSP_GTW, (addr[0], PORT_CLIENT))
|
||||||
|
logger.debug(f"Send ACK to {addr}")
|
||||||
|
|
||||||
|
except Exception as err:
|
||||||
|
sock.close()
|
||||||
|
logger.error(err)
|
||||||
|
|
||||||
|
|
||||||
|
def start_discovery_client():
|
||||||
|
print("Started discovery client")
|
||||||
|
sock = create_udp_socket()
|
||||||
|
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 str(addr[0]) in discovered_devices: continue
|
||||||
|
if data == DISCOVERY_RSP_GTW or data == DISCOVERY_RSP_MSH:
|
||||||
|
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,20 @@ import sys
|
|||||||
import json
|
import json
|
||||||
from data_class import Data
|
from data_class import Data
|
||||||
from devices import Device
|
from devices import Device
|
||||||
|
from logger import get_logger
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
DEBUG = True if os.getenv('DEBUG') == 'true' else False
|
|
||||||
|
def generate_json(device:Device):
|
||||||
|
dev, data_obj, from_config = device
|
||||||
|
return {
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def log_to_json(devices):
|
def log_to_json(devices):
|
||||||
@@ -15,27 +27,23 @@ def log_to_json(devices):
|
|||||||
data_obj: Data
|
data_obj: Data
|
||||||
from_config: Device
|
from_config: Device
|
||||||
file_name = f'{workdir}/data/{str(data_obj.mac).replace(":", "-")}.json'
|
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:
|
try:
|
||||||
with open(file_name, 'r') as file: data = json.load(file)
|
with open(file_name, 'r') as file: data = json.load(file)
|
||||||
except:
|
except:
|
||||||
with open(file_name, 'w') as file: file.write("[]")
|
with open(file_name, 'w') as file:
|
||||||
data = []
|
new_file = {
|
||||||
|
"name": from_config.name if from_config is not None else "Unknown",
|
||||||
measurements = {
|
"room": from_config.room if from_config is not None else "Unknown",
|
||||||
"timestamp": data_obj.timestamp,
|
"measurements": []
|
||||||
"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)
|
file.write(json.dumps(new_file))
|
||||||
|
data = new_file
|
||||||
|
|
||||||
print(measurements) if DEBUG else {}
|
data['measurements'].append(generate_json(device))
|
||||||
|
|
||||||
|
# logger.debug(measurements)
|
||||||
|
|
||||||
with open(file_name, 'w') as file: file.write(json.dumps(data, indent=2))
|
with open(file_name, 'w') as file: file.write(json.dumps(data, indent=2))
|
||||||
|
|
||||||
@@ -44,6 +52,5 @@ def log_to_mongodb(data):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def log_to_mqtt(data):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|||||||
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,11 +1,25 @@
|
|||||||
from time import sleep
|
from time import sleep
|
||||||
from log_data import log_to_json
|
from log_data import log_to_json
|
||||||
from discovery import start_discovery
|
from mqtt import publish_home_assistant_device_config
|
||||||
|
from mqtt import publish_device_state
|
||||||
|
from devices import Device
|
||||||
|
from data_class import Data
|
||||||
|
|
||||||
|
from ble_discovery import start_discovery
|
||||||
|
from logger import get_logger
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def publish_data(devices):
|
||||||
|
log_to_json(devices)
|
||||||
|
for device in devices:
|
||||||
|
publish_home_assistant_device_config(device)
|
||||||
|
publish_device_state(device)
|
||||||
|
|
||||||
|
|
||||||
def start_loop(interval=40, timeout=20):
|
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:
|
while True:
|
||||||
devices = start_discovery(timeout=timeout)
|
devices = start_discovery(timeout=timeout)
|
||||||
log_to_json(devices)
|
publish_data(devices)
|
||||||
sleep(interval)
|
sleep(interval)
|
||||||
|
|||||||
@@ -1,26 +1,33 @@
|
|||||||
import os
|
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 log_data import log_to_json
|
||||||
from loop import start_loop
|
from loop import start_loop
|
||||||
|
from check_update import check_for_update
|
||||||
|
from check_update import print_state
|
||||||
|
|
||||||
INTERVAL = 40
|
INTERVAL = 40
|
||||||
TIMEOUT = 20
|
TIMEOUT = 20
|
||||||
DOCKER = True if os.getenv('DOCKER') == 'true' else False
|
DOCKER = os.getenv('DOCKER') == 'true'
|
||||||
DEBUG = True if os.getenv('DEBUG') == 'true' else False
|
DEBUG = os.getenv('DEBUG')
|
||||||
interval = os.getenv('LOOP')
|
interval = os.getenv('LOOP')
|
||||||
timeout = os.getenv('TIMEOUT')
|
timeout = os.getenv('TIMEOUT')
|
||||||
|
|
||||||
if DEBUG:
|
logger = get_logger(__name__)
|
||||||
print(f"INTERVAL: {INTERVAL}")
|
|
||||||
print(f"TIMEOUT: {TIMEOUT}")
|
logger.debug(f"INTERVAL: {INTERVAL}")
|
||||||
print(f"interval: {interval}")
|
logger.debug(f"TIMEOUT: {TIMEOUT}")
|
||||||
print(f"timeout: {timeout}")
|
logger.debug(f"interval: {interval}")
|
||||||
print(f"DOCKER: {DOCKER}")
|
logger.debug(f"timeout: {timeout}")
|
||||||
print(f"DEBUG: {DEBUG}")
|
logger.debug(f"DOCKER: {DOCKER}")
|
||||||
print("")
|
logger.debug(f"DEBUG: {DEBUG}")
|
||||||
|
logger.debug(f"VERSION: {os.getenv('VERSION')}")
|
||||||
|
|
||||||
|
update_state = check_for_update()
|
||||||
|
print_state(update_state)
|
||||||
|
|
||||||
if DOCKER:
|
if DOCKER:
|
||||||
print("Running in docker")
|
logger.info('Running in Docker')
|
||||||
|
|
||||||
try:INTERVAL = int(interval)
|
try:INTERVAL = int(interval)
|
||||||
except:pass
|
except:pass
|
||||||
|
|||||||
@@ -1 +1,105 @@
|
|||||||
# TODO
|
import os
|
||||||
|
import json
|
||||||
|
import paho.mqtt.client as mqtt
|
||||||
|
|
||||||
|
from devices import Device
|
||||||
|
from data_class import Data
|
||||||
|
from logger import get_logger
|
||||||
|
|
||||||
|
topic_root = "homeassistant"
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
client = mqtt.Client(client_id='atc_mithermometer_gateway')
|
||||||
|
IP = os.getenv('MQTT_IP')
|
||||||
|
PORT = os.getenv('MQTT_PORT')
|
||||||
|
if IP and PORT:
|
||||||
|
client.connect(IP, int(PORT), 60)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_json(device:Device):
|
||||||
|
dev, data_obj, from_config = device
|
||||||
|
return {
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def generate_config_payloads(device:Device):
|
||||||
|
mac = '-'.join(device.mac.split(':')[3:])
|
||||||
|
|
||||||
|
state_topic = f"atc/device/{mac}/state"
|
||||||
|
|
||||||
|
device_json = {
|
||||||
|
"name": f"ATC {device.name}",
|
||||||
|
"identifiers":[f"atc-{mac}"],
|
||||||
|
}
|
||||||
|
|
||||||
|
config_temperature_payload = {
|
||||||
|
"name": f"{device.name} temperature",
|
||||||
|
"unique_id": f"atc-{mac}-temperature",
|
||||||
|
"state_topic": state_topic,
|
||||||
|
"unit_of_measurement": "°C",
|
||||||
|
"value_template": "{{ value_json.temperature}}",
|
||||||
|
"device": device_json
|
||||||
|
}
|
||||||
|
|
||||||
|
config_humidity_payload = {
|
||||||
|
"name": f"{device.name} humidity",
|
||||||
|
"unique_id": f"atc-{mac}-humidity",
|
||||||
|
"state_topic": state_topic,
|
||||||
|
"unit_of_measurement": "%",
|
||||||
|
"value_template": "{{ value_json.humidity}}",
|
||||||
|
"device": device_json
|
||||||
|
}
|
||||||
|
|
||||||
|
config_battery_percent_payload = {
|
||||||
|
"name": f"{device.name} battery",
|
||||||
|
"unique_id": f"atc-{mac}-battery-percent",
|
||||||
|
"state_topic": state_topic,
|
||||||
|
"unit_of_measurement": "%",
|
||||||
|
"value_template": "{{ value_json.battery_percent}}",
|
||||||
|
"device": device_json
|
||||||
|
}
|
||||||
|
|
||||||
|
config_rssi_payload = {
|
||||||
|
"name": f"{device.name} rssi",
|
||||||
|
"unique_id": f"atc-{mac}-rssi",
|
||||||
|
"state_topic": state_topic,
|
||||||
|
"value_template": "{{ value_json.rssi}}",
|
||||||
|
"device": device_json
|
||||||
|
}
|
||||||
|
|
||||||
|
return [config_temperature_payload, config_humidity_payload, config_battery_percent_payload, config_rssi_payload]
|
||||||
|
|
||||||
|
|
||||||
|
def publish_device_state(device:Device):
|
||||||
|
dev, data_obj, from_config = device
|
||||||
|
data_obj: Data
|
||||||
|
from_config: Device
|
||||||
|
mac = '-'.join(from_config.mac.split(':')[3:])
|
||||||
|
state_topic = f"atc/device/{mac}/state"
|
||||||
|
payload = generate_json(device)
|
||||||
|
logger.info(f"Publishing {payload}")
|
||||||
|
client.publish(state_topic, json.dumps(payload))
|
||||||
|
|
||||||
|
|
||||||
|
def publish_home_assistant_device_config(device:Device):
|
||||||
|
dev, data_obj, from_config = device
|
||||||
|
data_obj: Data
|
||||||
|
from_config: Device
|
||||||
|
mac = '-'.join(from_config.mac.split(':')[3:])
|
||||||
|
topic = f"{topic_root}/sensor/atc-{mac}/config"
|
||||||
|
|
||||||
|
temperature_payload, humidity_payload, battery_percent, rssi_payload = generate_config_payloads(from_config)
|
||||||
|
client.publish(f'{topic}-temperature/config', json.dumps(temperature_payload))
|
||||||
|
client.publish(f'{topic}-humidity/config', json.dumps(humidity_payload))
|
||||||
|
client.publish(f'{topic}-battery/config', json.dumps(battery_percent))
|
||||||
|
client.publish(f'{topic}-rssi/config', json.dumps(rssi_payload))
|
||||||
|
|
||||||
|
# client.publish(f'{topic}-temperature/config', json.dumps({}))
|
||||||
|
# client.publish(f'{topic}-humidity/config', json.dumps({}))
|
||||||
|
# client.publish(f'{topic}-battery/config', json.dumps({}))
|
||||||
|
# client.publish(f'{topic}-rssi/config', json.dumps({}))
|
||||||
|
|||||||
2
python/src/start_discovery_server.py
Normal file
2
python/src/start_discovery_server.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from gateway_discovery import start_discovery_server
|
||||||
|
start_discovery_server()
|
||||||
@@ -1,18 +1,10 @@
|
|||||||
TAG="latest"
|
TAG="latest"
|
||||||
CONTAINER="dasmoorhuhn/atc-mithermometer-gateway"
|
CONTAINER="dasmoorhuhn/atc-mithermometer-gateway"
|
||||||
CONTAINER_NAME="ATC_MiThermometer_Gateway"
|
CONTAINER_NAME="ATC_MiThermometer_Gateway"
|
||||||
VOLUME=data
|
VOLUME=$(pwd)/data
|
||||||
|
|
||||||
BACKGROUND=""
|
HELP="Using any command line argument except -d bypasses the .env file\n\n
|
||||||
TIME_ZONE=""
|
USAGE: sh run_docker.sh [OPTIONS] \n
|
||||||
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
|
[ -d ] Run in Backgrund \n
|
||||||
[ -t | --tag ] Set a docker tag. Default: latest \n
|
[ -t | --tag ] Set a docker tag. Default: latest \n
|
||||||
[ -b | --build ] Build the image before running the container \n
|
[ -b | --build ] Build the image before running the container \n
|
||||||
@@ -20,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
|
[ -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
|
[ -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
|
[ -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
|
[ -tz | --timezone ] Set the timezone. Default is Europe/Berlin \n
|
||||||
[ -to | --timeout ] Set the timeout for the bluetooth scan. default is 20s \n
|
[ -to | --timeout ] Set the timeout for the bluetooth scan. default is 20s \n
|
||||||
[ -h | --help ] Get this dialog \n
|
[ -h | --help ] Get this dialog \n
|
||||||
@@ -31,12 +25,50 @@ if [ "$?" != 0 ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
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() {
|
docker_run() {
|
||||||
sudo killall -9 bluetoothd > /dev/null 2>&1
|
sudo killall -9 bluetoothd > /dev/null 2>&1
|
||||||
echo Killing old container...
|
echo Killing old container...
|
||||||
docker stop $CONTAINER_NAME > /dev/null 2>&1
|
docker stop $CONTAINER_NAME > /dev/null 2>&1
|
||||||
docker container rm $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="docker run $BACKGROUND"
|
||||||
COMMAND="$COMMAND --cap-add=SYS_ADMIN"
|
COMMAND="$COMMAND --cap-add=SYS_ADMIN"
|
||||||
COMMAND="$COMMAND --cap-add=NET_ADMIN"
|
COMMAND="$COMMAND --cap-add=NET_ADMIN"
|
||||||
@@ -45,6 +77,15 @@ docker_run() {
|
|||||||
COMMAND="$COMMAND --restart=on-failure"
|
COMMAND="$COMMAND --restart=on-failure"
|
||||||
COMMAND="$COMMAND --volume=/var/run/dbus/:/var/run/dbus/"
|
COMMAND="$COMMAND --volume=/var/run/dbus/:/var/run/dbus/"
|
||||||
COMMAND="$COMMAND --volume=$VOLUME:/src/data"
|
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
|
if [ "$INTERACTIVE" = true ]; then
|
||||||
COMMAND="$COMMAND --interactive"
|
COMMAND="$COMMAND --interactive"
|
||||||
@@ -73,7 +114,15 @@ docker_run() {
|
|||||||
COMMAND="$COMMAND --env API=$API"
|
COMMAND="$COMMAND --env API=$API"
|
||||||
fi
|
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 --env DEBUG=$DEBUG"
|
||||||
COMMAND="$COMMAND $CONTAINER:$TAG"
|
COMMAND="$COMMAND $CONTAINER:$TAG"
|
||||||
echo
|
echo
|
||||||
@@ -81,6 +130,7 @@ docker_run() {
|
|||||||
echo
|
echo
|
||||||
echo DEBUG MODE
|
echo DEBUG MODE
|
||||||
else
|
else
|
||||||
|
COMMAND="$COMMAND --env DEBUG=$DEBUG"
|
||||||
COMMAND="$COMMAND $CONTAINER:$TAG"
|
COMMAND="$COMMAND $CONTAINER:$TAG"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -93,12 +143,18 @@ docker_run() {
|
|||||||
|
|
||||||
while [ "$1" != "" ]; do
|
while [ "$1" != "" ]; do
|
||||||
case $1 in
|
case $1 in
|
||||||
|
-se | --skip-env-file )
|
||||||
|
SKIP_ENV=true
|
||||||
|
echo "Skip env file"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
-d )
|
-d )
|
||||||
BACKGROUND="-d"
|
BACKGROUND="--detach"
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
--debug )
|
--debug )
|
||||||
DEBUG=true
|
shift
|
||||||
|
DEBUG=$1
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
-a | --api)
|
-a | --api)
|
||||||
@@ -114,6 +170,15 @@ while [ "$1" != "" ]; do
|
|||||||
VOLUME=$1
|
VOLUME=$1
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
|
-n | --name )
|
||||||
|
shift
|
||||||
|
NAME=$1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-m2 | --mesh-gateway)
|
||||||
|
MODE=2
|
||||||
|
shift
|
||||||
|
;;
|
||||||
-tz | --timezone )
|
-tz | --timezone )
|
||||||
shift
|
shift
|
||||||
TIME_ZONE=$1
|
TIME_ZONE=$1
|
||||||
|
|||||||
Reference in New Issue
Block a user