From f6b6e99457ec7613a4a0ba7e0e2d22c87b1559df Mon Sep 17 00:00:00 2001 From: Maximilian Giller Date: Mon, 6 May 2024 22:00:54 +0200 Subject: [PATCH 01/37] Integrated bed scale --- bettwaage-plotter/bett.py | 27 ---- src-new/endpoints/bettwaage.py | 117 --------------- src/endpoints/bettwaage.py | 56 +++++++ src/endpoints/handlers/bett.py | 149 +++++++++++++++++++ {src-new => src}/endpoints/handlers/hue.py | 0 {src-new => src}/endpoints/hue.py | 0 {src-new => src}/hue_bridge_registered.txt | 0 {src-new => src}/main.py | 2 + {src-new => src}/old_philips_hue_examples.py | 0 9 files changed, 207 insertions(+), 144 deletions(-) delete mode 100644 bettwaage-plotter/bett.py delete mode 100644 src-new/endpoints/bettwaage.py create mode 100644 src/endpoints/bettwaage.py create mode 100644 src/endpoints/handlers/bett.py rename {src-new => src}/endpoints/handlers/hue.py (100%) rename {src-new => src}/endpoints/hue.py (100%) rename {src-new => src}/hue_bridge_registered.txt (100%) rename {src-new => src}/main.py (82%) rename {src-new => src}/old_philips_hue_examples.py (100%) diff --git a/bettwaage-plotter/bett.py b/bettwaage-plotter/bett.py deleted file mode 100644 index 2e40e9e..0000000 --- a/bettwaage-plotter/bett.py +++ /dev/null @@ -1,27 +0,0 @@ -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 diff --git a/src-new/endpoints/bettwaage.py b/src-new/endpoints/bettwaage.py deleted file mode 100644 index 50cbbe0..0000000 --- a/src-new/endpoints/bettwaage.py +++ /dev/null @@ -1,117 +0,0 @@ -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) diff --git a/src/endpoints/bettwaage.py b/src/endpoints/bettwaage.py new file mode 100644 index 0000000..efaf8b7 --- /dev/null +++ b/src/endpoints/bettwaage.py @@ -0,0 +1,56 @@ +import asyncio +from fastapi.responses import HTMLResponse, JSONResponse +from fastapi import APIRouter + +import os +import csv +from handlers.bett import file_path, local_history, log_bed_weights + + +router = APIRouter() +asyncio.create_task(log_bed_weights()) + + +@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) -> list[dict]: + 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.get("/latest") +async def get_latest(): + if len(local_history) == 0: + return HTMLResponse(status_code=200, content="No data given yet") + return JSONResponse(local_history[-1]) + + +@router.delete("/delete", tags=["file"]) +async def delete_file(): + os.remove(file_path) + return "Deleted file" diff --git a/src/endpoints/handlers/bett.py b/src/endpoints/handlers/bett.py new file mode 100644 index 0000000..50d4c40 --- /dev/null +++ b/src/endpoints/handlers/bett.py @@ -0,0 +1,149 @@ +import asyncio +from datetime import datetime +import math +import os +from statistics import median +from typing import Optional +import requests as r +from ..hue import hue + +file_path: str = "bettwaage.csv" +header: str = "timestamp;tl;tr;bl;br;total;" + +bett_ip: str = "http://192.168.178.110:80" +matrix_clock_api: str = "http://192.168.178.84:8000" + +empty_weight: Optional[float] = None +local_history = [] +history_max_length: int = 24 * 60 * 60 # 24 hours +min_noticable_difference: float= 25 # In kg +show_scale_countdown: int = 0 # Number of updates for the scale, until return to clock + +average_person_weight: float = 75 + +is_warning_active: bool = False +leg_capacity_limit_patterns = [ + {"limit": 80, "pattern": 110, "duration": 1000}, + {"limit": 90, "pattern": 110, "duration": 250}, + {"limit": 100, "pattern": 10, "duration": 50}, +] + + + +def get_clusters(data: list[float], min_delta: float) -> dict: + clusters = {} + for point in data: + for known in clusters.keys(): + if math.abs(point - known) < min_delta: + clusters[known].append(point) + continue + clusters[point] = [point] + return clusters + +def show_time(): + r.post(f"{matrix_clock_api}/time") + +def show_scale(weight:float): + r.post(f"{matrix_clock_api}/message", json={ + "message": f"{weight:3.1f}kg" +}) + + +def is_capacity_reached() -> bool: + latest = local_history[-1] + highest_limit = None + for value in [latest["tl"], latest["tr"], latest["br"], latest["bl"]]: + for limit in leg_capacity_limit_patterns: + if value >= limit["limit"] and ( + highest_limit is None or limit["limit"] > highest_limit["limit"] + ): + highest_limit = limit + + global is_warning_active + if highest_limit is None: + if is_warning_active: + is_warning_active = False + show_time() + return + + is_warning_active = True + r.post(f"{matrix_clock_api}/pattern?pattern={highest_limit["pattern"]}&step_ms={highest_limit["duration"]}&contrast=255") + +def check_for_change(): + # Check for capicity limits + if is_capacity_reached(): + return + + global show_scale_countdown + latest = local_history[-1] + if show_scale_countdown > 0 and show_scale_countdown % 3 == 0: + show_scale(latest["total"]) + show_scale_countdown -= 1 + + # Is triggered? + delta = latest["total"] - local_history[-2]["total"] + if math.abs(delta) < min_noticable_difference: + return + + # Changed weight up or down? + weight_increased = delta > 0 + + # Make sure there is a bed_weight + global empty_weight + if empty_weight is None: + clusters = get_clusters(local_history) + empty_weight = min([median(cluster) for cluster in clusters.values()]) + + # Determine number of people + number_of_people = round((latest["total"] - empty_weight) / average_person_weight) + + if number_of_people == 1 and weight_increased: + show_scale_countdown = 60 # Should be a multiple of 3 + elif number_of_people >= 2 and weight_increased: + show_scale_countdown = 0 + show_time() + hue.in_room_activate_scene("Max Zimmer", "Sexy") + elif number_of_people == 1 and not weight_increased: + hue.in_room_activate_scene("Max Zimmer", "Tageslicht") + else: + show_scale_countdown = 0 + show_time() + + +def add_line_to_bed_history(line: str) -> None: + if not os.path.exists(file_path): + add_line_to_bed_history(header) + with open(file_path, "a") as fp: + fp.write(line + "\n") + + +def add_weights_to_log(tl: float, tr: float, bl: float, br: float): + total = tl + tr + bl + br + timestamp = datetime.now() + + global local_history + local_history.append( + {"tl": tl, "tr": tr, "bl": bl, "br": br, "total": total, "timestamp": timestamp} + ) + if len(local_history): + local_history = local_history[len(local_history) - history_max_length :] + + add_line_to_bed_history(f"{str(timestamp)};{tl};{tr};{bl};{br};{total};") + check_for_change() + + +async def log_bed_weights(): + while True: + try: + tl = r.get(f"{bett_ip}/sensor/tl/").json()["value"] + tr = r.get(f"{bett_ip}/sensor/tr/").json()["value"] + # bl = r.get(f"{bett_ip}/sensor/bl/").json()["value"] + br = r.get(f"{bett_ip}/sensor/br/").json()["value"] + + # Remove later + bl = br + + add_weights_to_log(tl, tr, bl, br) + except: + pass + await asyncio.sleep(60) diff --git a/src-new/endpoints/handlers/hue.py b/src/endpoints/handlers/hue.py similarity index 100% rename from src-new/endpoints/handlers/hue.py rename to src/endpoints/handlers/hue.py diff --git a/src-new/endpoints/hue.py b/src/endpoints/hue.py similarity index 100% rename from src-new/endpoints/hue.py rename to src/endpoints/hue.py diff --git a/src-new/hue_bridge_registered.txt b/src/hue_bridge_registered.txt similarity index 100% rename from src-new/hue_bridge_registered.txt rename to src/hue_bridge_registered.txt diff --git a/src-new/main.py b/src/main.py similarity index 82% rename from src-new/main.py rename to src/main.py index 28fcb83..b4cf79a 100644 --- a/src-new/main.py +++ b/src/main.py @@ -1,6 +1,8 @@ +import asyncio from fastapi import FastAPI from endpoints.hue import router as hue_router from endpoints.bettwaage import router as bettwaage_router +from endpoints.handlers.bett import log_bed_weights app = FastAPI() diff --git a/src-new/old_philips_hue_examples.py b/src/old_philips_hue_examples.py similarity index 100% rename from src-new/old_philips_hue_examples.py rename to src/old_philips_hue_examples.py From 3ced1992a5a98f7c2558788bd09024a7942cefc9 Mon Sep 17 00:00:00 2001 From: Maximilian Giller Date: Mon, 6 May 2024 22:05:21 +0200 Subject: [PATCH 02/37] Fixed relative import --- src/endpoints/bettwaage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/endpoints/bettwaage.py b/src/endpoints/bettwaage.py index efaf8b7..8b8a38b 100644 --- a/src/endpoints/bettwaage.py +++ b/src/endpoints/bettwaage.py @@ -4,7 +4,7 @@ from fastapi import APIRouter import os import csv -from handlers.bett import file_path, local_history, log_bed_weights +from .handlers.bett import file_path, local_history, log_bed_weights router = APIRouter() From ab5a1925247f252dede84ad9c918d64aea1376e7 Mon Sep 17 00:00:00 2001 From: Maximilian Giller Date: Mon, 6 May 2024 22:07:30 +0200 Subject: [PATCH 03/37] Fixed format string quotes --- src/endpoints/handlers/bett.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/endpoints/handlers/bett.py b/src/endpoints/handlers/bett.py index 50d4c40..e4c2631 100644 --- a/src/endpoints/handlers/bett.py +++ b/src/endpoints/handlers/bett.py @@ -15,9 +15,9 @@ matrix_clock_api: str = "http://192.168.178.84:8000" empty_weight: Optional[float] = None local_history = [] -history_max_length: int = 24 * 60 * 60 # 24 hours -min_noticable_difference: float= 25 # In kg -show_scale_countdown: int = 0 # Number of updates for the scale, until return to clock +history_max_length: int = 24 * 60 * 60 # 24 hours +min_noticable_difference: float = 25 # In kg +show_scale_countdown: int = 0 # Number of updates for the scale, until return to clock average_person_weight: float = 75 @@ -29,7 +29,6 @@ leg_capacity_limit_patterns = [ ] - def get_clusters(data: list[float], min_delta: float) -> dict: clusters = {} for point in data: @@ -40,13 +39,13 @@ def get_clusters(data: list[float], min_delta: float) -> dict: clusters[point] = [point] return clusters + def show_time(): r.post(f"{matrix_clock_api}/time") -def show_scale(weight:float): - r.post(f"{matrix_clock_api}/message", json={ - "message": f"{weight:3.1f}kg" -}) + +def show_scale(weight: float): + r.post(f"{matrix_clock_api}/message", json={"message": f"{weight:3.1f}kg"}) def is_capacity_reached() -> bool: @@ -65,15 +64,18 @@ def is_capacity_reached() -> bool: is_warning_active = False show_time() return - + is_warning_active = True - r.post(f"{matrix_clock_api}/pattern?pattern={highest_limit["pattern"]}&step_ms={highest_limit["duration"]}&contrast=255") + r.post( + f"{matrix_clock_api}/pattern?pattern={highest_limit['pattern']}&step_ms={highest_limit['duration']}&contrast=255" + ) + def check_for_change(): # Check for capicity limits if is_capacity_reached(): return - + global show_scale_countdown latest = local_history[-1] if show_scale_countdown > 0 and show_scale_countdown % 3 == 0: @@ -93,12 +95,12 @@ def check_for_change(): if empty_weight is None: clusters = get_clusters(local_history) empty_weight = min([median(cluster) for cluster in clusters.values()]) - + # Determine number of people number_of_people = round((latest["total"] - empty_weight) / average_person_weight) - + if number_of_people == 1 and weight_increased: - show_scale_countdown = 60 # Should be a multiple of 3 + show_scale_countdown = 60 # Should be a multiple of 3 elif number_of_people >= 2 and weight_increased: show_scale_countdown = 0 show_time() From 07c6ff492edd3c525e0439de94f8edd3605c9380 Mon Sep 17 00:00:00 2001 From: Maximilian Giller Date: Mon, 6 May 2024 22:11:24 +0200 Subject: [PATCH 04/37] Adjusted bed measurement frequency to 1 hz --- src/endpoints/handlers/bett.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/endpoints/handlers/bett.py b/src/endpoints/handlers/bett.py index e4c2631..1efb5d2 100644 --- a/src/endpoints/handlers/bett.py +++ b/src/endpoints/handlers/bett.py @@ -148,4 +148,4 @@ async def log_bed_weights(): add_weights_to_log(tl, tr, bl, br) except: pass - await asyncio.sleep(60) + await asyncio.sleep(1) From 78a5b3d8ca003f6eb5420945989f0091ad0275e9 Mon Sep 17 00:00:00 2001 From: Maximilian Giller Date: Mon, 6 May 2024 22:13:57 +0200 Subject: [PATCH 05/37] Fixed some type problems with local history --- src/endpoints/handlers/bett.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/endpoints/handlers/bett.py b/src/endpoints/handlers/bett.py index 1efb5d2..c3f7e76 100644 --- a/src/endpoints/handlers/bett.py +++ b/src/endpoints/handlers/bett.py @@ -93,7 +93,7 @@ def check_for_change(): # Make sure there is a bed_weight global empty_weight if empty_weight is None: - clusters = get_clusters(local_history) + clusters = get_clusters([d["total"] for d in local_history]) empty_weight = min([median(cluster) for cluster in clusters.values()]) # Determine number of people @@ -124,9 +124,7 @@ def add_weights_to_log(tl: float, tr: float, bl: float, br: float): timestamp = datetime.now() global local_history - local_history.append( - {"tl": tl, "tr": tr, "bl": bl, "br": br, "total": total, "timestamp": timestamp} - ) + local_history.append({"tl": tl, "tr": tr, "bl": bl, "br": br, "total": total}) if len(local_history): local_history = local_history[len(local_history) - history_max_length :] From 1819c9db9e8213069f1a8eba771ce6f914f4d04e Mon Sep 17 00:00:00 2001 From: Maximilian Giller Date: Mon, 6 May 2024 22:14:34 +0200 Subject: [PATCH 06/37] Fixed contrast reference --- src/endpoints/handlers/bett.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/endpoints/handlers/bett.py b/src/endpoints/handlers/bett.py index c3f7e76..a4f27d1 100644 --- a/src/endpoints/handlers/bett.py +++ b/src/endpoints/handlers/bett.py @@ -67,7 +67,7 @@ def is_capacity_reached() -> bool: is_warning_active = True r.post( - f"{matrix_clock_api}/pattern?pattern={highest_limit['pattern']}&step_ms={highest_limit['duration']}&contrast=255" + f"{matrix_clock_api}/pattern?pattern={highest_limit['pattern']}&step_ms={highest_limit['duration']}" ) From 9a9d40db826c10b60cbd86294563923cceae67a5 Mon Sep 17 00:00:00 2001 From: Maximilian Giller Date: Mon, 6 May 2024 22:22:13 +0200 Subject: [PATCH 07/37] Fixed file create logic --- src/endpoints/handlers/bett.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/endpoints/handlers/bett.py b/src/endpoints/handlers/bett.py index a4f27d1..91f174d 100644 --- a/src/endpoints/handlers/bett.py +++ b/src/endpoints/handlers/bett.py @@ -8,7 +8,7 @@ import requests as r from ..hue import hue file_path: str = "bettwaage.csv" -header: str = "timestamp;tl;tr;bl;br;total;" +header: str = "timestamp;tl;tr;bl;br;total;\n" bett_ip: str = "http://192.168.178.110:80" matrix_clock_api: str = "http://192.168.178.84:8000" @@ -113,9 +113,10 @@ def check_for_change(): def add_line_to_bed_history(line: str) -> None: - if not os.path.exists(file_path): - add_line_to_bed_history(header) + exists = os.path.exists(file_path) with open(file_path, "a") as fp: + if not exists: + fp.write(header) fp.write(line + "\n") From c4e302aef276a39dc8b367fc3bcc4fa199efb81a Mon Sep 17 00:00:00 2001 From: Maximilian Giller Date: Mon, 6 May 2024 22:23:32 +0200 Subject: [PATCH 08/37] Fixed warning active flag --- src/endpoints/handlers/bett.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/endpoints/handlers/bett.py b/src/endpoints/handlers/bett.py index 91f174d..c53074b 100644 --- a/src/endpoints/handlers/bett.py +++ b/src/endpoints/handlers/bett.py @@ -21,7 +21,7 @@ show_scale_countdown: int = 0 # Number of updates for the scale, until return t average_person_weight: float = 75 -is_warning_active: bool = False +is_warning_active: int = 0 leg_capacity_limit_patterns = [ {"limit": 80, "pattern": 110, "duration": 1000}, {"limit": 90, "pattern": 110, "duration": 250}, @@ -61,14 +61,17 @@ def is_capacity_reached() -> bool: global is_warning_active if highest_limit is None: if is_warning_active: - is_warning_active = False + is_warning_active = 0 show_time() - return + return False - is_warning_active = True - r.post( - f"{matrix_clock_api}/pattern?pattern={highest_limit['pattern']}&step_ms={highest_limit['duration']}" - ) + if is_warning_active != highest_limit["limit"]: + is_warning_active = highest_limit["limit"] + r.post( + f"{matrix_clock_api}/pattern?pattern={highest_limit['pattern']}&step_ms={highest_limit['duration']}" + ) + + return True def check_for_change(): From fa4acda10deec781b1b5cdb0e3bf2a106ea35d77 Mon Sep 17 00:00:00 2001 From: Maximilian Giller Date: Mon, 6 May 2024 22:28:35 +0200 Subject: [PATCH 09/37] Fixed local history being deleted --- src/endpoints/handlers/bett.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/endpoints/handlers/bett.py b/src/endpoints/handlers/bett.py index c53074b..3cca9dd 100644 --- a/src/endpoints/handlers/bett.py +++ b/src/endpoints/handlers/bett.py @@ -129,11 +129,10 @@ def add_weights_to_log(tl: float, tr: float, bl: float, br: float): global local_history local_history.append({"tl": tl, "tr": tr, "bl": bl, "br": br, "total": total}) - if len(local_history): + if len(local_history) > history_max_length: local_history = local_history[len(local_history) - history_max_length :] add_line_to_bed_history(f"{str(timestamp)};{tl};{tr};{bl};{br};{total};") - check_for_change() async def log_bed_weights(): @@ -148,6 +147,7 @@ async def log_bed_weights(): bl = br add_weights_to_log(tl, tr, bl, br) + check_for_change() except: pass await asyncio.sleep(1) From 7736ff0397619c93b29ba0601cafd70e23c4e51d Mon Sep 17 00:00:00 2001 From: Maximilian Giller Date: Mon, 6 May 2024 22:48:30 +0200 Subject: [PATCH 10/37] BL available --- src/endpoints/handlers/bett.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/endpoints/handlers/bett.py b/src/endpoints/handlers/bett.py index 3cca9dd..e482472 100644 --- a/src/endpoints/handlers/bett.py +++ b/src/endpoints/handlers/bett.py @@ -140,12 +140,9 @@ async def log_bed_weights(): try: tl = r.get(f"{bett_ip}/sensor/tl/").json()["value"] tr = r.get(f"{bett_ip}/sensor/tr/").json()["value"] - # bl = r.get(f"{bett_ip}/sensor/bl/").json()["value"] + bl = r.get(f"{bett_ip}/sensor/bl/").json()["value"] br = r.get(f"{bett_ip}/sensor/br/").json()["value"] - # Remove later - bl = br - add_weights_to_log(tl, tr, bl, br) check_for_change() except: From 1b5c73127e807955cba416c43d95e650da544a7a Mon Sep 17 00:00:00 2001 From: Maximilian Giller Date: Mon, 6 May 2024 22:56:01 +0200 Subject: [PATCH 11/37] Added logging --- src/endpoints/handlers/bett.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/endpoints/handlers/bett.py b/src/endpoints/handlers/bett.py index e482472..1c66bdf 100644 --- a/src/endpoints/handlers/bett.py +++ b/src/endpoints/handlers/bett.py @@ -6,6 +6,7 @@ from statistics import median from typing import Optional import requests as r from ..hue import hue +import logging file_path: str = "bettwaage.csv" header: str = "timestamp;tl;tr;bl;br;total;\n" @@ -77,6 +78,7 @@ def is_capacity_reached() -> bool: def check_for_change(): # Check for capicity limits if is_capacity_reached(): + logging.info(f"Capacity reached") return global show_scale_countdown @@ -91,6 +93,7 @@ def check_for_change(): return # Changed weight up or down? + logging.info(f"Delta: {delta}") weight_increased = delta > 0 # Make sure there is a bed_weight @@ -98,9 +101,11 @@ def check_for_change(): if empty_weight is None: clusters = get_clusters([d["total"] for d in local_history]) empty_weight = min([median(cluster) for cluster in clusters.values()]) + logging.info(f"Empty weight: {empty_weight}") # Determine number of people number_of_people = round((latest["total"] - empty_weight) / average_person_weight) + logging.info(f"Number of people: {number_of_people}") if number_of_people == 1 and weight_increased: show_scale_countdown = 60 # Should be a multiple of 3 From 1779517c0fb015ef363dcef57af3ddfaf2a781be Mon Sep 17 00:00:00 2001 From: Maximilian Giller Date: Mon, 6 May 2024 22:58:43 +0200 Subject: [PATCH 12/37] Added more logging --- src/endpoints/handlers/bett.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/endpoints/handlers/bett.py b/src/endpoints/handlers/bett.py index 1c66bdf..1c5d12e 100644 --- a/src/endpoints/handlers/bett.py +++ b/src/endpoints/handlers/bett.py @@ -90,6 +90,7 @@ def check_for_change(): # Is triggered? delta = latest["total"] - local_history[-2]["total"] if math.abs(delta) < min_noticable_difference: + logging.info(f"Delta: {delta}") return # Changed weight up or down? From 42b593f7f3ac62cf8d60572def3ea61ccf9da122 Mon Sep 17 00:00:00 2001 From: Maximilian Giller Date: Mon, 6 May 2024 23:00:02 +0200 Subject: [PATCH 13/37] Added exception logging --- src/endpoints/handlers/bett.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/endpoints/handlers/bett.py b/src/endpoints/handlers/bett.py index 1c5d12e..5f9941a 100644 --- a/src/endpoints/handlers/bett.py +++ b/src/endpoints/handlers/bett.py @@ -151,6 +151,6 @@ async def log_bed_weights(): add_weights_to_log(tl, tr, bl, br) check_for_change() - except: - pass + except Exception as ex: + logging.exception(ex) await asyncio.sleep(1) From dc22afdfc8532865f32e14100de59dc165f828cf Mon Sep 17 00:00:00 2001 From: Maximilian Giller Date: Mon, 6 May 2024 23:02:37 +0200 Subject: [PATCH 14/37] Fixed math.abs not found --- src/endpoints/handlers/bett.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/endpoints/handlers/bett.py b/src/endpoints/handlers/bett.py index 5f9941a..2fc5e23 100644 --- a/src/endpoints/handlers/bett.py +++ b/src/endpoints/handlers/bett.py @@ -34,7 +34,7 @@ def get_clusters(data: list[float], min_delta: float) -> dict: clusters = {} for point in data: for known in clusters.keys(): - if math.abs(point - known) < min_delta: + if math.fabs(point - known) < min_delta: clusters[known].append(point) continue clusters[point] = [point] @@ -89,12 +89,10 @@ def check_for_change(): # Is triggered? delta = latest["total"] - local_history[-2]["total"] - if math.abs(delta) < min_noticable_difference: - logging.info(f"Delta: {delta}") + if math.fabs(delta) < min_noticable_difference: return # Changed weight up or down? - logging.info(f"Delta: {delta}") weight_increased = delta > 0 # Make sure there is a bed_weight From abd9ec221c6d13c9bc1a446a3e42657323bca2ac Mon Sep 17 00:00:00 2001 From: Maximilian Giller Date: Mon, 6 May 2024 23:04:49 +0200 Subject: [PATCH 15/37] Fixed cluster call --- src/endpoints/handlers/bett.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/endpoints/handlers/bett.py b/src/endpoints/handlers/bett.py index 2fc5e23..203c4a1 100644 --- a/src/endpoints/handlers/bett.py +++ b/src/endpoints/handlers/bett.py @@ -98,7 +98,9 @@ def check_for_change(): # Make sure there is a bed_weight global empty_weight if empty_weight is None: - clusters = get_clusters([d["total"] for d in local_history]) + clusters = get_clusters( + [d["total"] for d in local_history], min_noticable_difference + ) empty_weight = min([median(cluster) for cluster in clusters.values()]) logging.info(f"Empty weight: {empty_weight}") From a723ccf2f81f3ca249152d292107e654aa1ee63a Mon Sep 17 00:00:00 2001 From: Maximilian Giller Date: Mon, 6 May 2024 23:06:04 +0200 Subject: [PATCH 16/37] Fixed scale --- src/endpoints/handlers/bett.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/endpoints/handlers/bett.py b/src/endpoints/handlers/bett.py index 203c4a1..d593fb5 100644 --- a/src/endpoints/handlers/bett.py +++ b/src/endpoints/handlers/bett.py @@ -84,7 +84,7 @@ def check_for_change(): global show_scale_countdown latest = local_history[-1] if show_scale_countdown > 0 and show_scale_countdown % 3 == 0: - show_scale(latest["total"]) + show_scale(latest["total"] - empty_weight) show_scale_countdown -= 1 # Is triggered? From 49c641cb4665bb6b39cba9c35cadf3bfa7a85aab Mon Sep 17 00:00:00 2001 From: Maximilian Giller Date: Mon, 6 May 2024 23:06:51 +0200 Subject: [PATCH 17/37] Fixed global empty_weight --- src/endpoints/handlers/bett.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/endpoints/handlers/bett.py b/src/endpoints/handlers/bett.py index d593fb5..3a6baad 100644 --- a/src/endpoints/handlers/bett.py +++ b/src/endpoints/handlers/bett.py @@ -82,6 +82,7 @@ def check_for_change(): return global show_scale_countdown + global empty_weight latest = local_history[-1] if show_scale_countdown > 0 and show_scale_countdown % 3 == 0: show_scale(latest["total"] - empty_weight) @@ -96,7 +97,6 @@ def check_for_change(): weight_increased = delta > 0 # Make sure there is a bed_weight - global empty_weight if empty_weight is None: clusters = get_clusters( [d["total"] for d in local_history], min_noticable_difference From 0280825b9d89805b862f43ab2586ee845d7617c5 Mon Sep 17 00:00:00 2001 From: Maximilian Giller Date: Mon, 6 May 2024 23:13:06 +0200 Subject: [PATCH 18/37] Fixed scale countdown --- src/endpoints/handlers/bett.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/endpoints/handlers/bett.py b/src/endpoints/handlers/bett.py index 3a6baad..a383cea 100644 --- a/src/endpoints/handlers/bett.py +++ b/src/endpoints/handlers/bett.py @@ -22,7 +22,7 @@ show_scale_countdown: int = 0 # Number of updates for the scale, until return t average_person_weight: float = 75 -is_warning_active: int = 0 +is_warning_active: int = -1 leg_capacity_limit_patterns = [ {"limit": 80, "pattern": 110, "duration": 1000}, {"limit": 90, "pattern": 110, "duration": 250}, @@ -87,6 +87,9 @@ def check_for_change(): if show_scale_countdown > 0 and show_scale_countdown % 3 == 0: show_scale(latest["total"] - empty_weight) show_scale_countdown -= 1 + elif show_scale_countdown == 0: + show_scale_countdown -= 1 + show_time() # Is triggered? delta = latest["total"] - local_history[-2]["total"] @@ -116,9 +119,6 @@ def check_for_change(): hue.in_room_activate_scene("Max Zimmer", "Sexy") elif number_of_people == 1 and not weight_increased: hue.in_room_activate_scene("Max Zimmer", "Tageslicht") - else: - show_scale_countdown = 0 - show_time() def add_line_to_bed_history(line: str) -> None: From 139f2f642750ddb39ff1c057ebf4b099eecb1e3b Mon Sep 17 00:00:00 2001 From: Maximilian Giller Date: Mon, 6 May 2024 23:13:53 +0200 Subject: [PATCH 19/37] Improved warning patterns --- src/endpoints/handlers/bett.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/endpoints/handlers/bett.py b/src/endpoints/handlers/bett.py index a383cea..fd17b43 100644 --- a/src/endpoints/handlers/bett.py +++ b/src/endpoints/handlers/bett.py @@ -24,8 +24,8 @@ average_person_weight: float = 75 is_warning_active: int = -1 leg_capacity_limit_patterns = [ - {"limit": 80, "pattern": 110, "duration": 1000}, - {"limit": 90, "pattern": 110, "duration": 250}, + {"limit": 80, "pattern": 110, "duration": 250}, + {"limit": 90, "pattern": 110, "duration": 100}, {"limit": 100, "pattern": 10, "duration": 50}, ] From 2eaa5fd14e6540177417a7836da63d33544788a5 Mon Sep 17 00:00:00 2001 From: Maximilian Giller Date: Mon, 6 May 2024 23:22:13 +0200 Subject: [PATCH 20/37] Fixed scale --- src/endpoints/handlers/bett.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/endpoints/handlers/bett.py b/src/endpoints/handlers/bett.py index fd17b43..16a3328 100644 --- a/src/endpoints/handlers/bett.py +++ b/src/endpoints/handlers/bett.py @@ -84,8 +84,9 @@ def check_for_change(): global show_scale_countdown global empty_weight latest = local_history[-1] - if show_scale_countdown > 0 and show_scale_countdown % 3 == 0: - show_scale(latest["total"] - empty_weight) + if show_scale_countdown > 0: + if show_scale_countdown % 3 == 0: + show_scale(latest["total"] - empty_weight) show_scale_countdown -= 1 elif show_scale_countdown == 0: show_scale_countdown -= 1 From cf4894e0ba309ef074c5bf8fdacd7ae9b4244b13 Mon Sep 17 00:00:00 2001 From: Maximilian Giller Date: Tue, 7 May 2024 00:02:40 +0200 Subject: [PATCH 21/37] Adjusted plot script for new circumstances --- bettwaage-plotter/main.py | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/bettwaage-plotter/main.py b/bettwaage-plotter/main.py index afc76e8..cf0f72a 100644 --- a/bettwaage-plotter/main.py +++ b/bettwaage-plotter/main.py @@ -4,7 +4,7 @@ from datetime import datetime import json -file_path = "history.json" +file_path = None history_url = "http://192.168.178.84:9587/bettwaage/history" convert_time_to_seconds = True @@ -21,44 +21,40 @@ else: # Experiment: Solving for missing foot with known total weight -bed_weight = 78290 -person_weight = 63000 +bed_weight = 81 +person_weight = 63 known_total_weight = bed_weight + person_weight bed_only_weight = {} for d in data: - if d["total"] == bed_weight: + if d["total"] < bed_weight: bed_only_weight = { "tl": d["tl"], "tr": d["tr"], - "bl": bed_weight - (d["tl"] + d["tr"] + d["br"]), + "bl": d["bl"], "br": d["br"], } break in_bed_data = None -threshhold = 100000 +threshhold = 100.0 min_length = 100 +skip = 0 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: + elif in_bed_data is not None and len(in_bed_data) > 0: + if skip > 0: + in_bed_data = [] + skip -= 1 + elif 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 +# data = in_bed_data # Array data From 4fb7c8b4613fd6d6af5d4d1885ebb762eb3f8f3a Mon Sep 17 00:00:00 2001 From: Maximilian Giller Date: Tue, 7 May 2024 00:03:31 +0200 Subject: [PATCH 22/37] Fixed exception on start up --- src/endpoints/handlers/bett.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/endpoints/handlers/bett.py b/src/endpoints/handlers/bett.py index 16a3328..80cec89 100644 --- a/src/endpoints/handlers/bett.py +++ b/src/endpoints/handlers/bett.py @@ -81,6 +81,9 @@ def check_for_change(): logging.info(f"Capacity reached") return + if len(local_history) < 2: + return + global show_scale_countdown global empty_weight latest = local_history[-1] From 35443b7cc8f75bf9dd8f690f9b1c6909ed365203 Mon Sep 17 00:00:00 2001 From: Maximilian Giller Date: Tue, 7 May 2024 00:04:31 +0200 Subject: [PATCH 23/37] Added sanity check to clean up data --- src/endpoints/handlers/bett.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/endpoints/handlers/bett.py b/src/endpoints/handlers/bett.py index 80cec89..fd07f9e 100644 --- a/src/endpoints/handlers/bett.py +++ b/src/endpoints/handlers/bett.py @@ -153,8 +153,13 @@ async def log_bed_weights(): bl = r.get(f"{bett_ip}/sensor/bl/").json()["value"] br = r.get(f"{bett_ip}/sensor/br/").json()["value"] + # Sanity check + if min([tl, tr, bl, br]) <= 0: + continue + add_weights_to_log(tl, tr, bl, br) check_for_change() except Exception as ex: logging.exception(ex) - await asyncio.sleep(1) + finally: + await asyncio.sleep(1) From 33617ec7a3f313b64b9b42e17d55c8c07a1377c8 Mon Sep 17 00:00:00 2001 From: Maximilian Giller Date: Tue, 7 May 2024 14:41:00 +0200 Subject: [PATCH 24/37] Improved handling of show_time --- src/endpoints/handlers/bett.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/endpoints/handlers/bett.py b/src/endpoints/handlers/bett.py index fd07f9e..be627d7 100644 --- a/src/endpoints/handlers/bett.py +++ b/src/endpoints/handlers/bett.py @@ -91,9 +91,6 @@ def check_for_change(): if show_scale_countdown % 3 == 0: show_scale(latest["total"] - empty_weight) show_scale_countdown -= 1 - elif show_scale_countdown == 0: - show_scale_countdown -= 1 - show_time() # Is triggered? delta = latest["total"] - local_history[-2]["total"] @@ -115,11 +112,14 @@ def check_for_change(): number_of_people = round((latest["total"] - empty_weight) / average_person_weight) logging.info(f"Number of people: {number_of_people}") - if number_of_people == 1 and weight_increased: + # Show scale? + if number_of_people == 1 and weight_increased and show_scale_countdown == 0: show_scale_countdown = 60 # Should be a multiple of 3 - elif number_of_people >= 2 and weight_increased: + else: show_scale_countdown = 0 - show_time() + + # Make room sexy + if number_of_people >= 2 and weight_increased: hue.in_room_activate_scene("Max Zimmer", "Sexy") elif number_of_people == 1 and not weight_increased: hue.in_room_activate_scene("Max Zimmer", "Tageslicht") From a6bbf7ef4dd426f0b0a286c4ff61e1b685eee1db Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 7 May 2024 16:55:12 +0200 Subject: [PATCH 25/37] Improved in-bed sequence filtering --- bettwaage-plotter/main.py | 58 +++++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/bettwaage-plotter/main.py b/bettwaage-plotter/main.py index cf0f72a..e6eeaa5 100644 --- a/bettwaage-plotter/main.py +++ b/bettwaage-plotter/main.py @@ -9,24 +9,25 @@ history_url = "http://192.168.178.84:9587/bettwaage/history" convert_time_to_seconds = True -# Script -data = None - +# Get data +data = Nones if file_path is None: + print("Fetching data ...") data = requests.get(history_url) data = data.json() else: + print("Reading data ...") with open(file_path, "r") as fp: data = json.load(fp) -# Experiment: Solving for missing foot with known total weight -bed_weight = 81 -person_weight = 63 -known_total_weight = bed_weight + person_weight +print("Processing data ...") + +# Get rough value for empty bed weight per leg +rough_bed_weight = 80 bed_only_weight = {} for d in data: - if d["total"] < bed_weight: + if d["total"] < rough_bed_weight: bed_only_weight = { "tl": d["tl"], "tr": d["tr"], @@ -35,29 +36,28 @@ for d in data: } break -in_bed_data = None +# Collect all coherent sequences of someone being in bed +in_bed_datas: list[list[dict]] = [] +is_in_bed_sequence = False threshhold = 100.0 -min_length = 100 -skip = 0 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 and len(in_bed_data) > 0: - if skip > 0: - in_bed_data = [] - skip -= 1 - elif len(in_bed_data) < min_length: - in_bed_data = [] - else: - break + if not is_in_bed_sequence: + in_bed_datas.append([]) + is_in_bed_sequence = True + in_bed_datas[-1].append(d) + elif is_in_bed_sequence: + is_in_bed_sequence = False -# data = in_bed_data +# Pick latest with minimum length/duration +min_length = 100 +for sequence in in_bed_datas: + if len(sequence) >= min_length: + data = sequence -# Array data +# Prepare data for plotting x = [d["timestamp"] for d in data] x = [datetime.strptime(d, "%Y-%m-%d %H:%M:%S.%f") for d in x] @@ -80,12 +80,16 @@ right = [t + b for t, b in zip(tr, br)] 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"] +right_bed_only = bed_only_weight["tr"] + bed_only_weight["br"] +bottom_bed_only = bed_only_weight["br"] + bed_only_weight["bl"] position_over_time = [] -for t, l in zip(top, left): +for t, b, l, r in zip(top, bottom, left, right): + horizontal_weight = l - left_bed_only + r - right_bed_only + vertical_weight = t - top_bed_only + b - bottom_bed_only position_over_time.append( ( - bed_size[0] * (l - left_bed_only) / person_weight, - bed_size[1] * (t - top_bed_only) / person_weight, + bed_size[0] * (l - left_bed_only) / horizontal_weight, + bed_size[1] * (t - top_bed_only) / vertical_weight, ) ) From 69956d66eac58d2e3a4f844cade89b2941dbd81e Mon Sep 17 00:00:00 2001 From: Maximilian Giller Date: Wed, 8 May 2024 20:57:37 +0200 Subject: [PATCH 26/37] Fixed typo --- bettwaage-plotter/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bettwaage-plotter/main.py b/bettwaage-plotter/main.py index e6eeaa5..1817652 100644 --- a/bettwaage-plotter/main.py +++ b/bettwaage-plotter/main.py @@ -10,7 +10,7 @@ history_url = "http://192.168.178.84:9587/bettwaage/history" convert_time_to_seconds = True # Get data -data = Nones +data = None if file_path is None: print("Fetching data ...") data = requests.get(history_url) From c7932b2a71054af85a42e08a722daf88316a7723 Mon Sep 17 00:00:00 2001 From: Maximilian Giller Date: Wed, 8 May 2024 22:10:52 +0200 Subject: [PATCH 27/37] Implemented network api for devices away mode --- requirements.txt | 5 ++- src/endpoints/handlers/fritz.py | 73 +++++++++++++++++++++++++++++++++ src/main.py | 3 +- 3 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 src/endpoints/handlers/fritz.py diff --git a/requirements.txt b/requirements.txt index 3fd5bc7..22b4fca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,9 @@ -# For Philips Hue Counter +# For Philips Hue phue +# For Fritz.Box API +fritzconnection + # API fastapi uvicorn[standard] \ No newline at end of file diff --git a/src/endpoints/handlers/fritz.py b/src/endpoints/handlers/fritz.py new file mode 100644 index 0000000..2c7e091 --- /dev/null +++ b/src/endpoints/handlers/fritz.py @@ -0,0 +1,73 @@ +import asyncio +import logging +from fritzconnection import FritzConnection +from datetime import datetime +from ..hue import hue + + +refresh_every_seconds: int = 30 # Every x seconds devices are polled again +trigger_away_after_seconds: int = ( + 2 * 60 +) # After all away-devices are gone for x seconds, light is turned off +away_devices = ["B2:06:77:EE:A9:0F"] # Max' iPhone +macaddresses_to_track = ["B2:06:77:EE:A9:0F"] # Max' iPhone + +fritz_api = FritzConnection(address="192.168.178.1") + +# Referenced documentation: https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/hostsSCPD.pdf + + +devices_last_online = {} + + +def get_all_devices() -> list: + numberOfDevices = fritz_api.call_action("Hosts", "GetHostNumberOfEntries")[ + "NewHostNumberOfEntries" + ] + devices = [] + for i in range(numberOfDevices): + devices.append( + fritz_api.call_action("Hosts", "GetGenericHostEntry", NewIndex=i) + ) + return devices + + +def get_specific_device(mac_adress: str) -> dict: + return fritz_api.call_action( + "Hosts", "GetSpecificHostEntry", NewMACAddress=mac_adress + ) + + +def check_for_change(): + # Check if devices are away for away-mode + all_away = True + for device in away_devices: + last_online = devices_last_online[device] + if (datetime.now() - last_online).total_seconds() < trigger_away_after_seconds: + all_away = False + break + + # Execute away mode + if all_away: + hue.in_room_deactivate_lights("Max Zimmer") + + +async def track_network_devices(): + global devices_last_online + + # Initial values to avoid None + for macaddress in macaddresses_to_track: + devices_last_online[macaddress] = datetime(1970, 1, 1, 0, 0, 0) + + while True: + try: + for macaddress in macaddresses_to_track: + is_online = get_specific_device(macaddress)["NewActive"] + if is_online: + devices_last_online[macaddress] = datetime.now() + + check_for_change() + except Exception as ex: + logging.exception(ex) + finally: + await asyncio.sleep(refresh_every_seconds) diff --git a/src/main.py b/src/main.py index b4cf79a..8765c9f 100644 --- a/src/main.py +++ b/src/main.py @@ -2,7 +2,7 @@ import asyncio from fastapi import FastAPI from endpoints.hue import router as hue_router from endpoints.bettwaage import router as bettwaage_router -from endpoints.handlers.bett import log_bed_weights +from endpoints.handlers.fritz import track_network_devices app = FastAPI() @@ -10,4 +10,5 @@ app.include_router(hue_router, prefix="/hue", tags=["hue"]) app.include_router(bettwaage_router, prefix="/bettwaage", tags=["bett"]) if __name__ == "__main__": + asyncio.create_task(track_network_devices()) app.run() From 3eb99735ee64c99a094a339a8949f089b0aac142 Mon Sep 17 00:00:00 2001 From: Maximilian Giller Date: Thu, 9 May 2024 02:15:19 +0200 Subject: [PATCH 28/37] Added start.sh --- start.sh | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 start.sh diff --git a/start.sh b/start.sh new file mode 100644 index 0000000..5dc9d32 --- /dev/null +++ b/start.sh @@ -0,0 +1,5 @@ +cd /home/pi/mash-server/src + +source ../.env/bin/activate + +uvicorn main:app --reload --host 0.0.0.0 --port 9587 From 3c0c85ecaa47585435873f54c3d4d2e8ec406e85 Mon Sep 17 00:00:00 2001 From: Maximilian Giller Date: Thu, 9 May 2024 02:18:07 +0200 Subject: [PATCH 29/37] Added missing requirements --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 22b4fca..7ad3c25 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,5 +5,6 @@ phue fritzconnection # API +requests fastapi uvicorn[standard] \ No newline at end of file From bfa6c10aa0f8a0ba5bf2b1fab7b31b262a70161b Mon Sep 17 00:00:00 2001 From: Maximilian Giller Date: Thu, 9 May 2024 02:18:26 +0200 Subject: [PATCH 30/37] Explicified start.sh --- start.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/start.sh b/start.sh index 5dc9d32..232d657 100644 --- a/start.sh +++ b/start.sh @@ -2,4 +2,4 @@ cd /home/pi/mash-server/src source ../.env/bin/activate -uvicorn main:app --reload --host 0.0.0.0 --port 9587 +python3 -m uvicorn main:app --reload --host 0.0.0.0 --port 9587 From a32ad26d0b29dbf3f68e9217139f2960fcc382ec Mon Sep 17 00:00:00 2001 From: Maximilian Giller Date: Thu, 9 May 2024 02:29:44 +0200 Subject: [PATCH 31/37] Maybe some improvements --- src/endpoints/handlers/fritz.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/endpoints/handlers/fritz.py b/src/endpoints/handlers/fritz.py index 2c7e091..7b36a22 100644 --- a/src/endpoints/handlers/fritz.py +++ b/src/endpoints/handlers/fritz.py @@ -5,10 +5,8 @@ from datetime import datetime from ..hue import hue -refresh_every_seconds: int = 30 # Every x seconds devices are polled again -trigger_away_after_seconds: int = ( - 2 * 60 -) # After all away-devices are gone for x seconds, light is turned off +refresh_every_seconds: int = 15 # Every x seconds devices are polled again +trigger_away_after_seconds: int = 60 # After all away-devices are gone for x seconds away_devices = ["B2:06:77:EE:A9:0F"] # Max' iPhone macaddresses_to_track = ["B2:06:77:EE:A9:0F"] # Max' iPhone From ab249a679d099eabce6adfa9f6c06c2d3a97f7c7 Mon Sep 17 00:00:00 2001 From: Maximilian Giller Date: Thu, 9 May 2024 02:33:53 +0200 Subject: [PATCH 32/37] Maybe fixing background task for fritz --- src/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.py b/src/main.py index 8765c9f..18ce741 100644 --- a/src/main.py +++ b/src/main.py @@ -5,10 +5,10 @@ from endpoints.bettwaage import router as bettwaage_router from endpoints.handlers.fritz import track_network_devices app = FastAPI() +asyncio.create_task(track_network_devices()) app.include_router(hue_router, prefix="/hue", tags=["hue"]) app.include_router(bettwaage_router, prefix="/bettwaage", tags=["bett"]) if __name__ == "__main__": - asyncio.create_task(track_network_devices()) app.run() From dbf11551c5a271bb34a7d172ff0343e3cca0a361 Mon Sep 17 00:00:00 2001 From: Maximilian Giller Date: Thu, 9 May 2024 02:43:17 +0200 Subject: [PATCH 33/37] Fixed typo, Improved away mode trigger mechanism --- src/endpoints/handlers/fritz.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/endpoints/handlers/fritz.py b/src/endpoints/handlers/fritz.py index 7b36a22..8a8f8e4 100644 --- a/src/endpoints/handlers/fritz.py +++ b/src/endpoints/handlers/fritz.py @@ -7,6 +7,7 @@ from ..hue import hue refresh_every_seconds: int = 15 # Every x seconds devices are polled again trigger_away_after_seconds: int = 60 # After all away-devices are gone for x seconds +away_triggered = False away_devices = ["B2:06:77:EE:A9:0F"] # Max' iPhone macaddresses_to_track = ["B2:06:77:EE:A9:0F"] # Max' iPhone @@ -30,9 +31,9 @@ def get_all_devices() -> list: return devices -def get_specific_device(mac_adress: str) -> dict: +def get_specific_device(mac_address: str) -> dict: return fritz_api.call_action( - "Hosts", "GetSpecificHostEntry", NewMACAddress=mac_adress + "Hosts", "GetSpecificHostEntry", NewMACAddress=mac_address ) @@ -46,9 +47,13 @@ def check_for_change(): break # Execute away mode + global away_triggered if all_away: - hue.in_room_deactivate_lights("Max Zimmer") - + if not away_triggered: + away_triggered = True + hue.in_room_deactivate_lights("Max Zimmer") + else: + away_triggered = False async def track_network_devices(): global devices_last_online From be56ad195637935d93fb2c1d2f05481f0beb733e Mon Sep 17 00:00:00 2001 From: Maximilian Giller Date: Thu, 9 May 2024 03:08:22 +0200 Subject: [PATCH 34/37] Relaxed time intervals for device tracking --- src/endpoints/handlers/fritz.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/endpoints/handlers/fritz.py b/src/endpoints/handlers/fritz.py index 8a8f8e4..b578149 100644 --- a/src/endpoints/handlers/fritz.py +++ b/src/endpoints/handlers/fritz.py @@ -5,8 +5,10 @@ from datetime import datetime from ..hue import hue -refresh_every_seconds: int = 15 # Every x seconds devices are polled again -trigger_away_after_seconds: int = 60 # After all away-devices are gone for x seconds +refresh_every_seconds: int = 60 # Every x seconds devices are polled again +trigger_away_after_seconds: int = ( + 3 * 60 +) # After all away-devices are gone for x seconds away_triggered = False away_devices = ["B2:06:77:EE:A9:0F"] # Max' iPhone macaddresses_to_track = ["B2:06:77:EE:A9:0F"] # Max' iPhone From ddf32f02b2143291789a05e321e59aa2184f7819 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 16 May 2024 17:26:12 +0200 Subject: [PATCH 35/37] Removed some verbosity from the bed --- src/endpoints/handlers/bett.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/endpoints/handlers/bett.py b/src/endpoints/handlers/bett.py index be627d7..05732aa 100644 --- a/src/endpoints/handlers/bett.py +++ b/src/endpoints/handlers/bett.py @@ -18,14 +18,17 @@ empty_weight: Optional[float] = None local_history = [] history_max_length: int = 24 * 60 * 60 # 24 hours min_noticable_difference: float = 25 # In kg -show_scale_countdown: int = 0 # Number of updates for the scale, until return to clock +initial_scale_coutndown: int = ( + 0 # Number of updates for the scale, until return to clock, should be a multiple of 3 +) +current_scale_countdown: int = 0 average_person_weight: float = 75 is_warning_active: int = -1 leg_capacity_limit_patterns = [ - {"limit": 80, "pattern": 110, "duration": 250}, - {"limit": 90, "pattern": 110, "duration": 100}, + # {"limit": 80, "pattern": 110, "duration": 250}, + # {"limit": 90, "pattern": 110, "duration": 100}, {"limit": 100, "pattern": 10, "duration": 50}, ] @@ -84,13 +87,13 @@ def check_for_change(): if len(local_history) < 2: return - global show_scale_countdown + global current_scale_countdown global empty_weight latest = local_history[-1] - if show_scale_countdown > 0: - if show_scale_countdown % 3 == 0: + if current_scale_countdown > 0: + if current_scale_countdown % 3 == 0: show_scale(latest["total"] - empty_weight) - show_scale_countdown -= 1 + current_scale_countdown -= 1 # Is triggered? delta = latest["total"] - local_history[-2]["total"] @@ -113,10 +116,10 @@ def check_for_change(): logging.info(f"Number of people: {number_of_people}") # Show scale? - if number_of_people == 1 and weight_increased and show_scale_countdown == 0: - show_scale_countdown = 60 # Should be a multiple of 3 + if number_of_people == 1 and weight_increased and current_scale_countdown == 0: + current_scale_countdown = initial_scale_coutndown else: - show_scale_countdown = 0 + current_scale_countdown = 0 # Make room sexy if number_of_people >= 2 and weight_increased: From 41073fce500a9c2524fde6500cba86ff7b7a5d26 Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 17 May 2024 09:48:14 +0200 Subject: [PATCH 36/37] Some more bed data science --- bettwaage-plotter/data_analysis.ipynb | 118 ++++++++++++++++++++++++++ bettwaage-plotter/main.py | 72 ++++++++++------ bettwaage-plotter/requirements.txt | 1 + 3 files changed, 165 insertions(+), 26 deletions(-) create mode 100644 bettwaage-plotter/data_analysis.ipynb diff --git a/bettwaage-plotter/data_analysis.ipynb b/bettwaage-plotter/data_analysis.ipynb new file mode 100644 index 0000000..7ab495d --- /dev/null +++ b/bettwaage-plotter/data_analysis.ipynb @@ -0,0 +1,118 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "\n", + "with open(\"latest_history.json\", \"r\") as fp:\n", + " data = json.load(fp)" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [], + "source": [ + "data = data[int(-60 * 60 * 14.5):int(-60 * 60 * 13)]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Total bed weight: 78.98908\n" + ] + } + ], + "source": [ + "# Get rough value for empty bed weight per leg\n", + "rough_bed_weight = 80\n", + "bed_only_weight = {}\n", + "for d in data:\n", + " if d[\"total\"] < rough_bed_weight:\n", + " bed_only_weight = {\n", + " \"tl\": d[\"tl\"],\n", + " \"tr\": d[\"tr\"],\n", + " \"bl\": d[\"bl\"],\n", + " \"br\": d[\"br\"],\n", + " }\n", + " total_bed_only_weight = sum(bed_only_weight.values())\n", + " break\n", + "\n", + "print(f\"Total bed weight: {total_bed_only_weight}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [], + "source": [ + "y = [d[\"total\"] - total_bed_only_weight for d in data]\n", + "y = [(i if i > 10 else None) for i in y]\n", + "\n", + "x = [i for i in range(len(y))]" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "fig, ax = plt.subplots()\n", + "\n", + "ax.set_xlabel(\"Time (s)\")\n", + "ax.set_ylabel(\"Weight (kg)\")\n", + "\n", + "ax.plot(x, y, color=\"tab:blue\")\n", + "\n", + "plt.show()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/bettwaage-plotter/main.py b/bettwaage-plotter/main.py index 1817652..67c3610 100644 --- a/bettwaage-plotter/main.py +++ b/bettwaage-plotter/main.py @@ -2,19 +2,27 @@ import requests import matplotlib.pyplot as plt from datetime import datetime import json +import os +latest_history_path = "latest_history.json" -file_path = None +file_path = latest_history_path history_url = "http://192.168.178.84:9587/bettwaage/history" +focus_on_latest_bed_data = False + convert_time_to_seconds = True # Get data data = None -if file_path is None: +if file_path is None or not os.path.exists(file_path): print("Fetching data ...") data = requests.get(history_url) data = data.json() + + print("Saving latest data ...") + with open(latest_history_path, "w", encoding="UTF-8") as fp: + json.dump(data, fp) else: print("Reading data ...") with open(file_path, "r") as fp: @@ -34,36 +42,38 @@ for d in data: "bl": d["bl"], "br": d["br"], } + total_bed_only_weight = sum(bed_only_weight.values()) break -# Collect all coherent sequences of someone being in bed -in_bed_datas: list[list[dict]] = [] -is_in_bed_sequence = False -threshhold = 100.0 -for d in data: - t = d["total"] - if t >= threshhold: - if not is_in_bed_sequence: - in_bed_datas.append([]) - is_in_bed_sequence = True - in_bed_datas[-1].append(d) - elif is_in_bed_sequence: - is_in_bed_sequence = False +if focus_on_latest_bed_data: + # Collect all coherent sequences of someone being in bed + in_bed_datas: list[list[dict]] = [] + is_in_bed_sequence = False + threshhold = 100.0 + for d in data: + t = d["total"] + if t >= threshhold: + if not is_in_bed_sequence: + in_bed_datas.append([]) + is_in_bed_sequence = True + in_bed_datas[-1].append(d) + elif is_in_bed_sequence: + is_in_bed_sequence = False -# Pick latest with minimum length/duration -min_length = 100 -for sequence in in_bed_datas: - if len(sequence) >= min_length: - data = sequence + # Pick latest with minimum length/duration + min_length = 100 + for sequence in in_bed_datas: + if len(sequence) >= min_length: + data = sequence # Prepare data for plotting x = [d["timestamp"] for d in data] -x = [datetime.strptime(d, "%Y-%m-%d %H:%M:%S.%f") for d in x] +# 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] +# 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] @@ -76,6 +86,18 @@ left = [t + b for t, b in zip(tl, bl)] right = [t + b for t, b in zip(tr, br)] +fig, ax = plt.subplots() + +person_weight = [t - total_bed_only_weight for t in total] + +ax.set_xlabel("Time (s)") +ax.set_ylabel("Weight (kg)") + +ax.plot(x, person_weight, color="tab:blue") + +plt.show() +exit() + # Experiment: Calculate position over time bed_size = (140, 200) left_bed_only = bed_only_weight["tl"] + bed_only_weight["bl"] @@ -93,7 +115,6 @@ for t, b, l, r in zip(top, bottom, left, right): ) ) - # Plot data fig, (ax0, ax1) = plt.subplots(nrows=2) @@ -112,7 +133,6 @@ ax0.legend( ["Total", "Top Left", "Top Right", "Bottom Left", "Bottom Right", "Top", "Bottom"] ) - # Experiment: Plot position import math import colorsys diff --git a/bettwaage-plotter/requirements.txt b/bettwaage-plotter/requirements.txt index 74599f5..2912652 100644 --- a/bettwaage-plotter/requirements.txt +++ b/bettwaage-plotter/requirements.txt @@ -1,3 +1,4 @@ matplotlib requests numpy +PyQt5 From 679a6c2e257fba3693c561360e08a9b0aeead160 Mon Sep 17 00:00:00 2001 From: Maximilian Giller Date: Fri, 7 Jun 2024 23:32:56 +0200 Subject: [PATCH 37/37] Implemented sexy mode flag --- src/endpoints/handlers/bett.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/endpoints/handlers/bett.py b/src/endpoints/handlers/bett.py index 05732aa..2437376 100644 --- a/src/endpoints/handlers/bett.py +++ b/src/endpoints/handlers/bett.py @@ -25,11 +25,13 @@ current_scale_countdown: int = 0 average_person_weight: float = 75 +sexy_mode_detection: bool = False # Turn lights "sexy" if two people are in bed + is_warning_active: int = -1 leg_capacity_limit_patterns = [ # {"limit": 80, "pattern": 110, "duration": 250}, # {"limit": 90, "pattern": 110, "duration": 100}, - {"limit": 100, "pattern": 10, "duration": 50}, + # {"limit": 100, "pattern": 10, "duration": 50}, ] @@ -122,10 +124,11 @@ def check_for_change(): current_scale_countdown = 0 # Make room sexy - if number_of_people >= 2 and weight_increased: - hue.in_room_activate_scene("Max Zimmer", "Sexy") - elif number_of_people == 1 and not weight_increased: - hue.in_room_activate_scene("Max Zimmer", "Tageslicht") + if sexy_mode_detection: + if number_of_people >= 2 and weight_increased: + hue.in_room_activate_scene("Max Zimmer", "Sexy") + elif number_of_people == 1 and not weight_increased: + hue.in_room_activate_scene("Max Zimmer", "Tageslicht") def add_line_to_bed_history(line: str) -> None: