Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bdbfa7d5b4 | |||
| 77aa9cb8f0 |
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 8.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 22 KiB |
@ -3,8 +3,6 @@ FROM python:3.12-alpine3.20 AS pip_build_stage
|
||||
|
||||
COPY ./python/requierements.txt /
|
||||
|
||||
RUN pip3.12 install setuptools
|
||||
|
||||
RUN apk add \
|
||||
make \
|
||||
git \
|
||||
|
||||
@ -22,23 +22,22 @@ 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 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
|
||||
- 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:**
|
||||
- [WIP] MQTT publishing with discovery for homeassistant
|
||||
- [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
|
||||
- [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
|
||||
- [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] Implement other BLE Sensors
|
||||
|
||||
**Current State**
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
## Getting started
|
||||
|
||||
|
||||
@ -5,7 +5,6 @@ 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
|
||||
[ -r | --release ] Build a release. Provide the Tag. \n
|
||||
[ -h | --help ] Get this dialog"
|
||||
|
||||
docker buildx version
|
||||
@ -25,29 +24,6 @@ build_docker() {
|
||||
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
|
||||
case $1 in
|
||||
-t | --tag )
|
||||
@ -55,12 +31,6 @@ while [ "$1" != "" ]; do
|
||||
TAG=$1
|
||||
shift
|
||||
;;
|
||||
-r | --release )
|
||||
shift
|
||||
TAG=$1
|
||||
RELEASE=true
|
||||
shift
|
||||
;;
|
||||
-i | --image )
|
||||
shift
|
||||
IMAGE=$1
|
||||
@ -82,8 +52,4 @@ while [ "$1" != "" ]; do
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$RELEASE" = true ]; then
|
||||
build_release
|
||||
else
|
||||
build_docker
|
||||
fi
|
||||
build_docker
|
||||
@ -4,9 +4,7 @@ NAME=
|
||||
INTERACTIVE=true
|
||||
BUILD=true
|
||||
API=true
|
||||
DEBUG=DEBUG
|
||||
DEBUG=INFO
|
||||
MODE=1
|
||||
LOOP=20
|
||||
TIMEOUT=20
|
||||
MQTT_IP=
|
||||
MQTT_PORT=1883
|
||||
@ -1,8 +0,0 @@
|
||||
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
|
||||
@ -1,68 +0,0 @@
|
||||
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
|
||||
|
||||
|
||||
|
||||
@ -1,49 +0,0 @@
|
||||
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]
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
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
|
||||
@ -1,82 +0,0 @@
|
||||
"""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)
|
||||
@ -1,4 +1,3 @@
|
||||
import json
|
||||
import os
|
||||
from flask import Flask
|
||||
from flask import jsonify
|
||||
@ -7,8 +6,6 @@ from flask_cors import CORS
|
||||
from flask_cors import cross_origin
|
||||
from check_update import check_for_update
|
||||
|
||||
from data_class import LogEntry
|
||||
|
||||
|
||||
class API:
|
||||
"""
|
||||
@ -24,8 +21,8 @@ class API:
|
||||
@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'),
|
||||
@ -37,27 +34,16 @@ class API:
|
||||
"name": os.getenv('NAME'),
|
||||
"info": {
|
||||
"files_size_sum": self.get_file_size(),
|
||||
"devices": self.get_files()
|
||||
"files": os.listdir(f'{workdir}/data')
|
||||
}
|
||||
}
|
||||
return jsonify(root_dict)
|
||||
|
||||
@self.app.route('/api/<path:path>')
|
||||
@self.app.route('/api/json/<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')
|
||||
@cross_origin()
|
||||
def serve_index():
|
||||
@ -72,15 +58,6 @@ class API:
|
||||
if file.endswith('.json'): sizes += os.path.getsize(f'{workdir}/data/{file}')
|
||||
return sizes
|
||||
|
||||
def get_files(self):
|
||||
workdir, filename = os.path.split(os.path.abspath(__file__))
|
||||
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.app.run(host='0.0.0.0', port=8000)
|
||||
|
||||
@ -18,7 +18,7 @@ class ScanDelegate(DefaultDelegate):
|
||||
global devices
|
||||
|
||||
for (sdid, desc, val) in dev.getScanData():
|
||||
if not self.is_temperature(sdid, val): continue
|
||||
if self.is_temperature(sdid, val):
|
||||
data_obj = Data(val)
|
||||
|
||||
if self.is_atc_device(dev, data_obj):
|
||||
@ -27,7 +27,9 @@ class ScanDelegate(DefaultDelegate):
|
||||
|
||||
@staticmethod
|
||||
def is_temperature(sdid, val):
|
||||
return sdid == 22 and len(val) == 30
|
||||
if sdid != 22: return False
|
||||
if len(val) != 30: return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def is_atc_device(dev, data_obj):
|
||||
@ -38,8 +40,8 @@ class ScanDelegate(DefaultDelegate):
|
||||
|
||||
device_from_config = get_device(dev)
|
||||
|
||||
room = device_from_config.room if device_from_config.room is not None else '?'
|
||||
logger.info(f"Device: {dev.addr.upper()} ({dev.addrType}), RSSI: {dev.rssi}dB, Room: {room}")
|
||||
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
|
||||
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Config:
|
||||
API_PORT:int
|
||||
|
||||
@ -37,28 +37,3 @@ class Data:
|
||||
"battery_volt": self.battery_volt,
|
||||
"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
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
# https://github.com/jholtmann/ip_discovery
|
||||
"""Find other gateways and serve the udp socket"""
|
||||
|
||||
import os
|
||||
import FindMyIP
|
||||
@ -9,7 +8,6 @@ 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
|
||||
@ -18,46 +16,39 @@ PORT_SERVER = 9434
|
||||
PORT_CLIENT = 9435
|
||||
|
||||
|
||||
def create_udp_socket():
|
||||
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)
|
||||
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")
|
||||
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:
|
||||
sock.close()
|
||||
logger.error(err)
|
||||
sock.close()
|
||||
|
||||
|
||||
def start_discovery_client():
|
||||
print("Started discovery client")
|
||||
sock = 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)
|
||||
sock.settimeout(SOCKET_TIMEOUT)
|
||||
server_address = ('255.255.255.255', PORT_SERVER)
|
||||
start_time_stamp = get_unix_time()
|
||||
@ -66,16 +57,14 @@ def start_discovery_client():
|
||||
|
||||
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:
|
||||
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)
|
||||
|
||||
@ -7,18 +7,6 @@ from logger import get_logger
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
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):
|
||||
workdir, filename = os.path.split(os.path.abspath(__file__))
|
||||
|
||||
@ -32,18 +20,22 @@ def log_to_json(devices):
|
||||
try:
|
||||
with open(file_name, 'r') as file: data = json.load(file)
|
||||
except:
|
||||
with open(file_name, 'w') as file:
|
||||
new_file = {
|
||||
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 if from_config is not None else "Unknown",
|
||||
"room": from_config.room if from_config is not None else "Unknown",
|
||||
"measurements": []
|
||||
"room": from_config.room if from_config is not None else "Unknown"
|
||||
}
|
||||
file.write(json.dumps(new_file))
|
||||
data = new_file
|
||||
data.append(measurements)
|
||||
|
||||
data['measurements'].append(generate_json(device))
|
||||
|
||||
# logger.debug(measurements)
|
||||
logger.debug(measurements)
|
||||
|
||||
with open(file_name, 'w') as file: file.write(json.dumps(data, indent=2))
|
||||
|
||||
@ -52,5 +44,6 @@ def log_to_mongodb(data):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
def log_to_mqtt(data):
|
||||
pass
|
||||
|
||||
|
||||
@ -1,25 +1,13 @@
|
||||
from time import sleep
|
||||
from log_data import log_to_json
|
||||
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):
|
||||
logger.info(f"Starting loop with interval {interval}s")
|
||||
while True:
|
||||
devices = start_discovery(timeout=timeout)
|
||||
publish_data(devices)
|
||||
log_to_json(devices)
|
||||
sleep(interval)
|
||||
|
||||
@ -26,7 +26,8 @@ logger.debug(f"VERSION: {os.getenv('VERSION')}")
|
||||
update_state = check_for_update()
|
||||
print_state(update_state)
|
||||
|
||||
if DOCKER:
|
||||
try:
|
||||
if DOCKER:
|
||||
logger.info('Running in Docker')
|
||||
|
||||
try:INTERVAL = int(interval)
|
||||
@ -38,5 +39,7 @@ if DOCKER:
|
||||
if interval is None: log_to_json(start_discovery(timeout=TIMEOUT))
|
||||
else:start_loop(INTERVAL, TIMEOUT)
|
||||
|
||||
else:
|
||||
else:
|
||||
start_loop(interval=40)
|
||||
except Exception as err:
|
||||
logger.error(err)
|
||||
|
||||
@ -1,105 +1,15 @@
|
||||
import os
|
||||
import json
|
||||
import paho.mqtt.client as mqtt
|
||||
import paho.mqtt.client as MQTT
|
||||
|
||||
from devices import Device
|
||||
from data_class import Data
|
||||
from logger import get_logger
|
||||
topic_root = "/atc_mithermometer_gateway"
|
||||
|
||||
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)
|
||||
mqtt = MQTT.Client(mqtt.CallbackAPIVersion.VERSION2)
|
||||
mqtt.connect("192.168.178.140", 1883, 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 publish_measurement(mac):
|
||||
topic = f"{topic_root}/measurements/{mac}"
|
||||
topic_temp = f"{topic}/temperature"
|
||||
topic_humid = f"{topic}/humidity"
|
||||
topic_battery = f"{topic}/battery"
|
||||
|
||||
|
||||
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({}))
|
||||
mqtt.publish(topic_temp, )
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
from gateway_discovery import start_discovery_server
|
||||
from find_gateways import start_discovery_server
|
||||
start_discovery_server()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user