Compare commits

...

26 commits

Author SHA1 Message Date
Max
7fdbdb161e Merge branch 'bettwaage-sidequest' 2024-05-06 15:25:59 +02:00
Max
c823f2a40a Preparing for merge with old smart home 2024-05-06 15:20:14 +02:00
Max
965dd30665 Added proper color gradient to position plot 2024-05-06 15:19:12 +02:00
Max
9004125dd5 Position experiments 2024-05-06 01:03:34 +02:00
c84b53a374 Improved plotter script 2024-05-03 22:39:30 +02:00
2fb05d641b Working versions 2024-05-03 00:29:06 +02:00
21376509ff Implemented simple plotter 2024-05-02 20:06:47 +02:00
5bc507f38a Fixed add endpoint 2024-05-02 18:29:44 +02:00
eac465e1a8 Maybe fixed write endpoint? 2024-05-02 18:27:38 +02:00
ed1f6a7475 Fixed delete file endpoint 2024-05-02 18:17:42 +02:00
2ac70ddac6 Hopefully fixed write endpoint 2024-05-02 18:16:39 +02:00
cc8eb0c2a3 Fixed wrong word in response 2024-05-02 18:12:25 +02:00
636b14f206 Fixed history endpoint 2024-05-02 18:10:12 +02:00
7bb61ea905 Fixed http methods 2024-05-02 18:08:14 +02:00
63e1c29e52 Fixed edge case response 2024-05-02 18:05:40 +02:00
dd3264365c Added return values for each endpoint 2024-05-02 18:03:44 +02:00
10255b966b Fixed data missing case 2024-05-02 18:02:03 +02:00
368052f713 Added history endpoints 2024-05-02 18:00:27 +02:00
5e3261d1f7 Added latest endpoint 2024-05-02 17:53:49 +02:00
7b21fa130f Added tags 2024-05-02 17:49:06 +02:00
8916835654 Added calibration endpoints 2024-05-02 17:48:37 +02:00
918a0fa04c Fixed bettwaage csv insert 2024-05-02 17:40:11 +02:00
ee5de34647 Fixed file insert 2024-05-02 17:39:06 +02:00
30efc2b405 Fixed format string 2024-05-02 17:38:10 +02:00
07fe365709 Fixed format string 2024-05-02 17:36:34 +02:00
47bd8c2fdd Added basic bettwaage endpoints 2024-05-02 17:31:38 +02:00
19 changed files with 435 additions and 2 deletions

3
.gitignore vendored
View file

@ -161,5 +161,6 @@ cython_debug/
.vscode/settings.json
.vscode/launch.json
# Custom
hue_bridge_registered.txt
history.json

27
bettwaage-plotter/bett.py Normal file
View file

@ -0,0 +1,27 @@
import requests as r
from time import sleep
bett_ip = "http://192.168.178.110:80"
mash_ip = "http://192.168.178.84:9587"
while True:
try:
tl = r.get(f"{bett_ip}/sensor/tl/").json()["value"]
tr = r.get(f"{bett_ip}/sensor/tr/").json()["value"]
br = r.get(f"{bett_ip}/sensor/br/").json()["value"]
print(f"tl = {tl}")
print(f"tr = {tr}")
# print(f"tl = {tl}")
print(f"br = {br}")
print("==========")
print(f"total = {tl + tr + br * 2}")
print("==========")
s = r.post(f"{mash_ip}/bettwaage/add?tl={int(tl * 1000)}&tr={int(tr * 1000)}&bl={int(br * 1000)}&br={int(br * 1000)}")
sleep(1)
except KeyboardInterrupt:
exit()
except:
pass

137
bettwaage-plotter/main.py Normal file
View file

@ -0,0 +1,137 @@
import requests
import matplotlib.pyplot as plt
from datetime import datetime
import json
file_path = "history.json"
history_url = "http://192.168.178.84:9587/bettwaage/history"
convert_time_to_seconds = True
# Script
data = None
if file_path is None:
data = requests.get(history_url)
data = data.json()
else:
with open(file_path, "r") as fp:
data = json.load(fp)
# Experiment: Solving for missing foot with known total weight
bed_weight = 78290
person_weight = 63000
known_total_weight = bed_weight + person_weight
bed_only_weight = {}
for d in data:
if d["total"] == bed_weight:
bed_only_weight = {
"tl": d["tl"],
"tr": d["tr"],
"bl": bed_weight - (d["tl"] + d["tr"] + d["br"]),
"br": d["br"],
}
break
in_bed_data = None
threshhold = 100000
min_length = 100
for d in data:
t = d["total"]
if t >= threshhold:
if in_bed_data is None:
in_bed_data = []
in_bed_data.append(d)
elif in_bed_data is not None:
if len(in_bed_data) < min_length:
in_bed_data = []
else:
break
# Calculate bottom left
for d in data:
d["bl"] = known_total_weight - (d["br"] + d["tr"] + d["tl"])
# Set known total weight
d["total"] = known_total_weight
data = in_bed_data
# Array data
x = [d["timestamp"] for d in data]
x = [datetime.strptime(d, "%Y-%m-%d %H:%M:%S.%f") for d in x]
if convert_time_to_seconds:
max_time = max(x)
x = [(d - max_time).total_seconds() for d in x]
total = [d["total"] for d in data]
tl = [d["tl"] for d in data]
tr = [d["tr"] for d in data]
bl = [d["bl"] for d in data]
br = [d["br"] for d in data]
top = [l + r for l, r in zip(tl, tr)]
bottom = [l + r for l, r in zip(bl, br)]
left = [t + b for t, b in zip(tl, bl)]
right = [t + b for t, b in zip(tr, br)]
# Experiment: Calculate position over time
bed_size = (140, 200)
left_bed_only = bed_only_weight["tl"] + bed_only_weight["bl"]
top_bed_only = bed_only_weight["tr"] + bed_only_weight["tl"]
position_over_time = []
for t, l in zip(top, left):
position_over_time.append(
(
bed_size[0] * (l - left_bed_only) / person_weight,
bed_size[1] * (t - top_bed_only) / person_weight,
)
)
# Plot data
fig, (ax0, ax1) = plt.subplots(nrows=2)
ax0.set_xlabel("Time (s)")
ax0.set_ylabel("Weight (kg)")
ax0.plot(x, total, color="tab:blue")
ax0.plot(x, tl, color="tab:red")
ax0.plot(x, tr, color="tab:green")
ax0.plot(x, bl, color="tab:orange")
ax0.plot(x, br, color="tab:purple")
ax0.plot(x, top, color="tab:pink")
ax0.plot(x, bottom, color="tab:grey")
ax0.legend(
["Total", "Top Left", "Top Right", "Bottom Left", "Bottom Right", "Top", "Bottom"]
)
# Experiment: Plot position
import math
import colorsys
segments = 100
seg_length = math.ceil(len(position_over_time) / segments)
horizontal, vertical = zip(*position_over_time)
for i in range(0, segments):
low = int(i * seg_length)
high = min(int((i + 1) * seg_length), len(position_over_time))
ax1.plot(
horizontal[low:high],
vertical[low:high],
color=colorsys.hsv_to_rgb(i / segments * 0.5, 1, 0.7),
linewidth=0.3,
)
ax1.set_xlim((0, bed_size[0]))
ax1.set_ylim((0, bed_size[1]))
ax1.invert_yaxis()
plt.show()

View file

@ -0,0 +1,3 @@
matplotlib
requests
numpy

View file

@ -0,0 +1,117 @@
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi import APIRouter
from datetime import datetime
import os
import csv
router = APIRouter()
file_path = "bettwaage.csv"
header = "timestamp;tl;tr;bl;br;total;"
latest_values = []
zero_values = [0, 0, 0, 0]
scale_values = [1, 1, 1, 1]
def add_line_to_history(line: str) -> None:
with open(file_path, "a") as fp:
fp.write(line + "\n")
def convert_to_weight(value: int, zero_value: int, scale: float) -> float:
return (value - zero_value) * scale
@router.get("/file", tags=["file"])
async def get_file():
with open(file_path, "r", encoding="UTF-8") as fp:
return HTMLResponse("\n".join(fp.readlines()))
@router.get("/history")
async def get_history(count: int = None) -> []:
points = []
with open(file_path, "r", encoding="UTF-8") as fp:
reader = csv.DictReader(fp, delimiter=";")
for row in reader:
if not row:
continue
points.append(
{
"timestamp": row["timestamp"],
"total": float(row["total"]),
"tl": float(row["tl"]),
"tr": float(row["tr"]),
"bl": float(row["bl"]),
"br": float(row["br"]),
}
)
if count:
return points[-count]
else:
return points
@router.post("/add")
async def add_weight(tl: int, tr: int, bl: int, br: int):
global latest_values
latest_values = [tl, tr, bl, br]
tl = convert_to_weight(tl, zero_values[0], scale_values[0])
tr = convert_to_weight(tr, zero_values[1], scale_values[1])
bl = convert_to_weight(bl, zero_values[2], scale_values[2])
br = convert_to_weight(br, zero_values[3], scale_values[3])
sum = tl + tr + bl + br
add_line_to_history(f"{str(datetime.now())};{tl};{tr};{bl};{br};{sum};")
return "Added data"
@router.get("/latest")
async def get_latest():
if not latest_values:
return HTMLResponse(status_code=200, content="No data given yet")
total = sum(latest_values)
return JSONResponse(
{
"tl": latest_values[0],
"tr": latest_values[1],
"bl": latest_values[2],
"br": latest_values[3],
"total": total,
}
)
@router.delete("/delete", tags=["file"])
async def delete_file():
os.remove(file_path)
add_line_to_history(header)
return "Deleted file and created new file with headers"
@router.post("/zero", tags=["calibration"])
async def set_zero():
if not latest_values:
return HTMLResponse(
status_code=400, content="Requiring data before setting zeros."
)
global zero_values
zero_values = latest_values
return "Set zeroes to: " + " | ".join(str(v) for v in zero_values)
@router.post("/scales", tags=["calibration"])
async def set_scales(tl: float, tr: float, bl: float, br: float):
global scale_values
scale_values = [tl, tr, bl, br]
return "Set scales to: " + " | ".join(str(v) for v in scale_values)
if not os.path.exists(file_path):
add_line_to_history(header)

View file

@ -0,0 +1,86 @@
from time import sleep
from phue import Bridge
from pathlib import Path
class HueAdapter:
"""Handler for Hue API calls."""
registered_ips_file = "hue_bridge_registered.txt"
def __init__(self, bridge_ip: str):
"""Initialize the HueHandler."""
self.bridge = None
self.connect(bridge_ip)
def connect(self, bridge_ip: str):
if bridge_ip in self.get_registered_ips():
self.bridge = Bridge(bridge_ip)
self.bridge.connect()
return
# Connect loop
while True:
try:
self.bridge = Bridge(bridge_ip)
self.bridge.connect()
break
except Exception as e:
print(f"Failed to connect to bridge: {bridge_ip}")
print(e)
print("Trying again in 5 seconds..")
sleep(5)
self.register_bridge(bridge_ip)
def get_registered_ips(self) -> list:
"""Get a list of registered bridge IPs."""
if not Path(HueAdapter.registered_ips_file).is_file():
return []
with open(HueAdapter.registered_ips_file, "r") as f:
return [ad.strip() for ad in f.readlines()]
def register_bridge(self, bridge_ip: str):
"""Register a bridge IP."""
with open(HueAdapter.registered_ips_file, "a") as f:
f.write(bridge_ip + "\n")
def list_scenes(self) -> dict:
return self.bridge.get_scene()
def get_scene_by_name(self, name):
for key, scene in self.list_scenes().items():
if scene["name"] == name:
scene["id"] = key
return scene
return None
def in_room_activate_scene(self, room_name: str, scene_name: str):
"""Activate a scene in a room.
Args:
scene (str): The name of the scene to activate.
room (str): The name of the room to activate the scene in.
"""
scene_id = self.get_scene_by_name(scene_name)["id"]
if scene_id is None:
raise "Scene not found."
self.bridge.set_group(room_name, {"scene": scene_id})
def in_room_deactivate_lights(self, room_name: str):
"""Deactivate all lights in a room.
Args:
room_name (str): The name of the room to deactivate the lights in.
"""
self.bridge.set_group(room_name, {"on": False})
def in_room_activate_lights(self, room_name: str):
"""Activate all lights in a room.
Args:
room_name (str): The name of the room to activate the lights in.
"""
self.bridge.set_group(room_name, {"on": True})

View file

@ -0,0 +1,60 @@
from fastapi import FastAPI, APIRouter
from hue.hue_adapter import HueAdapter
from ..mash.feature import Feature
from fastapi import APIRouter
from fastapi.responses import HTMLResponse
router = APIRouter(tags=["hue"])
hue = HueAdapter("192.168.178.85")
########## Integration ##########
class HueIntegration(Feature):
def __init__(self) -> None:
super().__init__("hue")
def add_routes(self, server: FastAPI) -> None:
server.include_router(router, prefix="/hue")
########## Routes ##########
@router.get("/scenes", tags=["scene"])
async def get_scenes():
return hue.list_scenes()
@router.post(
"/room/{room_name}/scene/{scene_name}",
tags=["room", "scene"],
)
async def activate_scene(room_name: str, scene_name: str):
try:
hue.in_room_activate_scene(room_name, scene_name)
except Exception as e:
return HTMLResponse(status_code=400, content=str(e))
@router.post(
"/room/{room_name}/off",
tags=["room"],
)
async def deactivate_room(room_name: str):
try:
hue.in_room_deactivate_lights(room_name)
except Exception as e:
return HTMLResponse(status_code=400, content=str(e))
@router.post(
"/room/{room_name}/on",
tags=["room"],
)
async def activate_room(room_name: str):
try:
hue.in_room_activate_lights(room_name)
except Exception as e:
return HTMLResponse(status_code=400, content=str(e))

View file

@ -7,5 +7,7 @@ mash: MaSH = MaSH("config.yaml")
mash.add_module(HueModule())
mash.add_module(MatrixClockModule())
app.include_router(bettwaage_router, prefix="/bettwaage", tags=["bett"])
if __name__ == "__main__":
mash.run()