Merge branch 'bettwaage-sidequest'
This commit is contained in:
commit
7fdbdb161e
19 changed files with 435 additions and 2 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -161,5 +161,6 @@ cython_debug/
|
||||||
.vscode/settings.json
|
.vscode/settings.json
|
||||||
.vscode/launch.json
|
.vscode/launch.json
|
||||||
|
|
||||||
|
# Custom
|
||||||
hue_bridge_registered.txt
|
hue_bridge_registered.txt
|
||||||
|
history.json
|
||||||
|
|
27
bettwaage-plotter/bett.py
Normal file
27
bettwaage-plotter/bett.py
Normal 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
137
bettwaage-plotter/main.py
Normal 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()
|
3
bettwaage-plotter/requirements.txt
Normal file
3
bettwaage-plotter/requirements.txt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
matplotlib
|
||||||
|
requests
|
||||||
|
numpy
|
117
src-new/endpoints/bettwaage.py
Normal file
117
src-new/endpoints/bettwaage.py
Normal 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)
|
86
src-new/hue/hue_adapter.py
Normal file
86
src-new/hue/hue_adapter.py
Normal 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})
|
60
src-new/hue/hue_feature.py
Normal file
60
src-new/hue/hue_feature.py
Normal 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))
|
|
@ -7,5 +7,7 @@ mash: MaSH = MaSH("config.yaml")
|
||||||
mash.add_module(HueModule())
|
mash.add_module(HueModule())
|
||||||
mash.add_module(MatrixClockModule())
|
mash.add_module(MatrixClockModule())
|
||||||
|
|
||||||
|
app.include_router(bettwaage_router, prefix="/bettwaage", tags=["bett"])
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
mash.run()
|
mash.run()
|
Loading…
Reference in a new issue